chore: import upstream snapshot with attribution
This commit is contained in:
+73
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from samples.concepts.memory.azure_ai_search_hotel_samples.data_model import (
|
||||
HotelSampleClass,
|
||||
custom_index,
|
||||
load_records,
|
||||
)
|
||||
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding
|
||||
from semantic_kernel.connectors.azure_ai_search import AzureAISearchCollection
|
||||
|
||||
"""
|
||||
With the data model and records defined in data_model.py, this script will create an Azure AI Search collection,
|
||||
upsert the records, and then search the collection using vector and hybrid search.
|
||||
The script will print the first five records in the collection and the search results.
|
||||
The script will also delete the collection at the end.
|
||||
|
||||
Note that we add the OpenAITextEmbedding to the collection, which is used to generate the vectors.
|
||||
To use the built-in embedding in Azure AI Search, remove this and add that definition to the custom_index.
|
||||
"""
|
||||
|
||||
|
||||
async def main(query: str):
|
||||
records = load_records()
|
||||
# Create the Azure AI Search collection
|
||||
async with AzureAISearchCollection[str, HotelSampleClass](
|
||||
record_type=HotelSampleClass, embedding_generator=OpenAITextEmbedding()
|
||||
) as collection:
|
||||
# Check if the collection exists.
|
||||
await collection.ensure_collection_exists(index=custom_index)
|
||||
await collection.upsert(records)
|
||||
# get the first five records to check the upsert worked.
|
||||
results = await collection.get(order_by="HotelName", top=5)
|
||||
print("Get first five records: ")
|
||||
if results:
|
||||
for result in results:
|
||||
print(
|
||||
f" {result.HotelId} (in {result.Address.City}, {result.Address.Country}): {result.Description}"
|
||||
)
|
||||
|
||||
print("\n")
|
||||
print("Search results using vector: ")
|
||||
# Use search to search using the vector.
|
||||
results = await collection.search(
|
||||
query,
|
||||
vector_property_name="DescriptionVector",
|
||||
)
|
||||
async for result in results.results:
|
||||
print(
|
||||
f" {result.record.HotelId} (in {result.record.Address.City}, "
|
||||
f"{result.record.Address.Country}): {result.record.Description} (score: {result.score})"
|
||||
)
|
||||
print("\n")
|
||||
print("Search results using hybrid: ")
|
||||
# Use hybrid search to search using the vector.
|
||||
results = await collection.hybrid_search(
|
||||
query,
|
||||
vector_property_name="DescriptionVector",
|
||||
additional_property_name="Description",
|
||||
)
|
||||
async for result in results.results:
|
||||
print(
|
||||
f" {result.record.HotelId} (in {result.record.Address.City}, "
|
||||
f"{result.record.Address.Country}): {result.record.Description} (score: {result.score})"
|
||||
)
|
||||
|
||||
await collection.ensure_collection_deleted()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
query = "swimming pool and good internet connection"
|
||||
asyncio.run(main(query=query))
|
||||
@@ -0,0 +1,226 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from samples.concepts.memory.azure_ai_search_hotel_samples.data_model import (
|
||||
HotelSampleClass,
|
||||
custom_index,
|
||||
load_records,
|
||||
)
|
||||
from semantic_kernel.agents import AgentThread, ChatCompletionAgent
|
||||
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
|
||||
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding
|
||||
from semantic_kernel.connectors.azure_ai_search import AzureAISearchCollection
|
||||
from semantic_kernel.filters import FilterTypes, FunctionInvocationContext
|
||||
from semantic_kernel.functions import KernelParameterMetadata, KernelPlugin
|
||||
from semantic_kernel.kernel_types import OptionalOneOrList
|
||||
|
||||
"""
|
||||
This sample builds on the previous one, but can be run independently.
|
||||
It uses the data model defined in data_model.py, and with that creates a collection
|
||||
and creates two kernel functions from those that are then made available to a LLM.
|
||||
The first function is a search function that allows you to search for hotels, optionally filtering for a city.
|
||||
The second function is a details function that allows you to get details about a hotel.
|
||||
"""
|
||||
|
||||
|
||||
# Create an Azure AI Search collection.
|
||||
collection = AzureAISearchCollection[str, HotelSampleClass](
|
||||
record_type=HotelSampleClass, embedding_generator=OpenAITextEmbedding()
|
||||
)
|
||||
# load the records
|
||||
records = load_records()
|
||||
# get the set of cities
|
||||
cities: set[str] = set()
|
||||
for record in records:
|
||||
if record.Address.Country == "USA" and record.Address.City:
|
||||
cities.add(record.Address.City)
|
||||
|
||||
|
||||
# Before we create the plugin, we want to create a function that will help the plugin work the way we want it to.
|
||||
# This function allows us to create the plugin with a parameter called `city` that
|
||||
# then get's put into a filter for address/city.
|
||||
# This function has to adhere to the `DynamicFilterFunction` signature.
|
||||
# which consists of 2 named arguments, `filter`, and `parameters`.
|
||||
# and kwargs.
|
||||
# It returns the updated filter.
|
||||
# The default version that is used when not supplying this, reads the parameters and if there is
|
||||
# a parameter that is not `query`, `top`, or 'skip`, and it can find a value for it, either in the kwargs
|
||||
# or the default value specified in the parameter, it will add a filter to the options.
|
||||
# In this case, we are adding a filter to the options to filter by the city, but since the technical name
|
||||
# of that field in the index is `address/city`, want to do this manually.
|
||||
# this can also be used to replace a complex technical name in your index with a friendly name towards the LLM.
|
||||
def filter_update(
|
||||
filter: OptionalOneOrList[Callable | str] | None = None,
|
||||
parameters: list["KernelParameterMetadata"] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> OptionalOneOrList[Callable | str] | None:
|
||||
if "city" in kwargs:
|
||||
city = kwargs["city"]
|
||||
if city not in cities:
|
||||
raise ValueError(f"City '{city}' is not in the list of cities: {', '.join(cities)}")
|
||||
# we need the actual value and not a named param, otherwise the parser will not be able to find it.
|
||||
new_filter = f"lambda x: x.Address.City == '{city}'"
|
||||
if filter is None:
|
||||
filter = new_filter
|
||||
elif isinstance(filter, list):
|
||||
filter.append(new_filter)
|
||||
else:
|
||||
filter = [filter, new_filter]
|
||||
return filter
|
||||
|
||||
|
||||
instructions = """You are a travel agent. Your name is Mosscap and
|
||||
you have one goal: help people find a hotel.
|
||||
Your full name, should you need to know it, is
|
||||
Splendid Speckled Mosscap. You communicate
|
||||
effectively, but you tend to answer with long
|
||||
flowery prose. You always make sure to include the
|
||||
hotel_id in your answers so that the user can
|
||||
use it to get more information."""
|
||||
|
||||
|
||||
search_plugin = KernelPlugin(
|
||||
name="azure_ai_search",
|
||||
description="A plugin that allows you to search for hotels in Azure AI Search.",
|
||||
functions=[
|
||||
collection.create_search_function(
|
||||
# this create search method uses the `search` method of the text search object.
|
||||
# remember that the text_search object for this sample is based on
|
||||
# the text_search method of the Azure AI Search.
|
||||
# but it can also be used with the other vector search methods.
|
||||
# This method's description, name and parameters are what will be serialized as part of the tool
|
||||
# call functionality of the LLM.
|
||||
# And crafting these should be part of the prompt design process.
|
||||
# The default parameters are `query`, `top`, and `skip`, but you specify your own.
|
||||
# The default parameters match the parameters of the VectorSearchOptions class.
|
||||
description="A hotel search engine, allows searching for hotels in specific cities, "
|
||||
"you do not have to specify that you are searching for hotels, for all, use `*`.",
|
||||
search_type="keyword_hybrid",
|
||||
# Next to the dynamic filters based on parameters, I can specify options that are always used.
|
||||
# this can include the `top` and `skip` parameters, but also filters that are always applied.
|
||||
# In this case, I am filtering by country, so only hotels in the USA are returned.
|
||||
filter=lambda x: x.Address.Country == "USA",
|
||||
parameters=[
|
||||
KernelParameterMetadata(
|
||||
name="query",
|
||||
description="What to search for.",
|
||||
type="str",
|
||||
is_required=True,
|
||||
type_object=str,
|
||||
),
|
||||
KernelParameterMetadata(
|
||||
name="city",
|
||||
description=f"The city that you want to search for a hotel in, values are: {', '.join(cities)}",
|
||||
type="str",
|
||||
type_object=str,
|
||||
),
|
||||
KernelParameterMetadata(
|
||||
name="top",
|
||||
description="Number of results to return.",
|
||||
type="int",
|
||||
default_value=5,
|
||||
type_object=int,
|
||||
),
|
||||
],
|
||||
# and here the above created function is passed in.
|
||||
filter_update_function=filter_update,
|
||||
# finally, we specify the `string_mapper` function that is used to convert the record to a string.
|
||||
# This is used to make sure the relevant information from the record is passed to the LLM.
|
||||
string_mapper=lambda x: f"(hotel_id :{x.record.HotelId}) {x.record.HotelName} (rating {x.record.Rating}) - {x.record.Description}. Address: {x.record.Address.StreetAddress}, {x.record.Address.City}, {x.record.Address.StateProvince}, {x.record.Address.Country}. Number of room types: {len(x.record.Rooms)}. Last renovated: {x.record.LastRenovationDate}.", # noqa: E501
|
||||
),
|
||||
collection.create_search_function(
|
||||
# This second function is a more detailed one, that uses a `HotelId` to get details about a hotel.
|
||||
# we set the top to 1, so that only 1 record is returned.
|
||||
function_name="get_details",
|
||||
description="Get details about a hotel, by ID, use the generic search function to get the ID.",
|
||||
top=1,
|
||||
parameters=[
|
||||
KernelParameterMetadata(
|
||||
name="HotelId",
|
||||
description="The hotel ID to get details for.",
|
||||
type="str",
|
||||
is_required=True,
|
||||
type_object=str,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Next we create the Agent, with two functions.
|
||||
travel_agent = ChatCompletionAgent(
|
||||
name="TravelAgent",
|
||||
description="A travel agent that helps you find a hotel.",
|
||||
service=OpenAIChatCompletion(),
|
||||
instructions=instructions,
|
||||
function_choice_behavior=FunctionChoiceBehavior.Auto(),
|
||||
plugins=[search_plugin],
|
||||
)
|
||||
|
||||
|
||||
# This filter will log all calls to the Azure AI Search plugin.
|
||||
# This allows us to see what parameters are being passed to the plugin.
|
||||
# And this gives us a way to debug the search experience and if necessary tweak the parameters and descriptions.
|
||||
@travel_agent.kernel.filter(filter_type=FilterTypes.FUNCTION_INVOCATION)
|
||||
async def log_search_filter(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
):
|
||||
print(f"Calling Azure AI Search ({context.function.name}) with arguments:")
|
||||
for arg in context.arguments:
|
||||
if arg in ("chat_history"):
|
||||
continue
|
||||
print(f' {arg}: "{context.arguments[arg]}"')
|
||||
await next(context)
|
||||
|
||||
|
||||
async def chat():
|
||||
# Create the Azure AI Search collection
|
||||
async with collection:
|
||||
# Check if the collection exists.
|
||||
await collection.ensure_collection_exists(index=custom_index)
|
||||
if not await collection.get(top=1):
|
||||
await collection.upsert(records)
|
||||
thread: AgentThread | None = None
|
||||
while True:
|
||||
try:
|
||||
user_input = input("User:> ")
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nExiting chat...")
|
||||
break
|
||||
except EOFError:
|
||||
print("\n\nExiting chat...")
|
||||
break
|
||||
|
||||
if user_input == "exit":
|
||||
print("\n\nExiting chat...")
|
||||
break
|
||||
|
||||
result = await travel_agent.get_response(messages=user_input, thread=thread)
|
||||
print(f"Agent: {result.content}")
|
||||
thread = result.thread
|
||||
|
||||
delete_collection = input("Do you want to delete the collection? (y/n): ")
|
||||
if delete_collection.lower() == "y":
|
||||
await collection.ensure_collection_deleted()
|
||||
print("Collection deleted.")
|
||||
else:
|
||||
print("Collection not deleted.")
|
||||
|
||||
|
||||
async def main():
|
||||
print(
|
||||
"Welcome to the chat bot!\
|
||||
\n Type 'exit' to exit.\
|
||||
\n Try to find a hotel to your liking!"
|
||||
)
|
||||
await chat()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,93 @@
|
||||
## Azure AI Search with Hotel Sample Data
|
||||
|
||||
This guide explains how to use the provided Python samples to set up your Azure AI Search index, load hotel data, and run search queries—all programmatically, without manual configuration in the Azure Portal.
|
||||
|
||||
### Overview
|
||||
|
||||
The Python samples in this folder will:
|
||||
|
||||
- Define the hotel data model and index schema.
|
||||
- Download and load the hotel sample data.
|
||||
- Create the Azure AI Search index (if it does not exist).
|
||||
- Upsert the data into your Azure AI Search index.
|
||||
- Run text, vector, and hybrid search queries.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- An Azure AI Search service instance.
|
||||
- OpenAI resource (for embedding generation), can be replaced with Azure OpenAI Embeddings.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Data Model and Index Creation**
|
||||
The data model and index schema are defined in `data_model.py`.
|
||||
This script is called by the other two, so no need to run manually.
|
||||
|
||||
2. **Loading Data and Generating Vectors**
|
||||
The script downloads hotel data from the Azure samples repository.
|
||||
It uses OpenAI to generate vector embeddings for hotel descriptions, which are stored in the index.
|
||||
|
||||
3. **Running the Sample**
|
||||
To run the main sample and see search results:
|
||||
|
||||
```bash
|
||||
python 1_interact_with_the_collection.py
|
||||
```
|
||||
|
||||
This will:
|
||||
- Create the index (if needed)
|
||||
- Load and upsert the hotel data
|
||||
- Get the first five records
|
||||
- Perform vector and hybrid search queries and print the results
|
||||
|
||||
4. **Customizing the Search**
|
||||
You can modify the search query in `1_interact_with_the_collection.py` by changing the `query` variable at the bottom of the script.
|
||||
|
||||
5. **Cleanup**
|
||||
The sample script deletes the index at the end of execution. You can comment out this step if you want to keep the index for further experimentation.
|
||||
|
||||
### Example Output
|
||||
|
||||
```python
|
||||
Get first five records:
|
||||
31 (in Nashville, USA): All of the suites feature full-sized kitchens stocked with cookware, separate living and sleeping areas and sofa beds. Some of the larger rooms have fireplaces and patios or balconies. Experience real country hospitality in the heart of bustling Nashville. The most vibrant music scene in the world is just outside your front door.
|
||||
23 (in Kirkland, USA): Mix and mingle in the heart of the city. Shop and dine, mix and mingle in the heart of downtown, where fab lake views unite with a cheeky design.
|
||||
3 (in Atlanta, USA): The Gastronomic Hotel stands out for its culinary excellence under the management of William Dough, who advises on and oversees all of the Hotel’s restaurant services.
|
||||
20 (in Albuquerque, USA): The Best Gaming Resort in the area. With elegant rooms & suites, pool, cabanas, spa, brewery & world-class gaming. This is the best place to play, stay & dine.
|
||||
45 (in Seattle, USA): The largest year-round resort in the area offering more of everything for your vacation – at the best value! What can you enjoy while at the resort, aside from the mile-long sandy beaches of the lake? Check out our activities sure to excite both young and young-at-heart guests. We have it all, including being named “Property of the Year” and a “Top Ten Resort” by top publications.
|
||||
|
||||
|
||||
Search results using vector:
|
||||
6 (in San Francisco, USA): Newest kid on the downtown block. Steps away from the most popular destinations in downtown, enjoy free WiFi, an indoor rooftop pool & fitness center, 24 Grab'n'Go & drinks at the bar (score: 0.6350645)
|
||||
27 (in Aventura, USA): Complimentary Airport Shuttle & WiFi. Book Now and save - Spacious All Suite Hotel, Indoor Outdoor Pool, Fitness Center, Florida Green certified, Complimentary Coffee, HDTV (score: 0.62773544)
|
||||
25 (in Metairie, USA): Newly Redesigned Rooms & airport shuttle. Minutes from the airport, enjoy lakeside amenities, a resort-style pool & stylish new guestrooms with Internet TVs. (score: 0.6193533)
|
||||
|
||||
|
||||
Search results using hybrid:
|
||||
25 (in Metairie, USA): Newly Redesigned Rooms & airport shuttle. Minutes from the airport, enjoy lakeside amenities, a resort-style pool & stylish new guestrooms with Internet TVs. (score: 0.03279569745063782)
|
||||
27 (in Aventura, USA): Complimentary Airport Shuttle & WiFi. Book Now and save - Spacious All Suite Hotel, Indoor Outdoor Pool, Fitness Center, Florida Green certified, Complimentary Coffee, HDTV (score: 0.032786883413791656)
|
||||
36 (in Memphis, USA): Stunning Downtown Hotel with indoor Pool. Ideally located close to theatres, museums and the convention center. Indoor Pool and Sauna and fitness centre. Popular Bar & Restaurant (score: 0.0317460335791111)
|
||||
```
|
||||
|
||||
### Advanced: Agent and Plugin Integration
|
||||
|
||||
For a more advanced example, see `2_use_as_a_plugin.py`, which demonstrates how to expose the hotel search as a plugin to a agent, this showcases how you can use the collection to create multiple search functions for different purposes and with some set filters and customized output. It then uses those in an Agent to help the user.
|
||||
|
||||
### Advanced: Use the Azure AI Search integrated embedding generation
|
||||
|
||||
For more info on this topic, see the [Azure AI Search documentation](https://learn.microsoft.com/en-us/azure/search/search-how-to-integrated-vectorization?tabs=prepare-data-storage%2Cprepare-model-aoai).
|
||||
|
||||
To use this, next to the steps needed to create the embedding skillset, you need to:
|
||||
|
||||
1. Adapt the `vectorizers` list and the profiles list in `custom_index` in `data_model.py`.
|
||||
1. Remove the `embedding_generator` param from the collection in both scripts.
|
||||
By removing this, we indicate that the embedding generation takes place in the service.
|
||||
|
||||
---
|
||||
|
||||
**Note:**
|
||||
You no longer need to manually configure the index or upload data via the Azure Portal. All setup is handled by the Python code.
|
||||
|
||||
If you encounter issues, ensure your Azure credentials and endpoints are correctly configured in your environment.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,344 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
import requests
|
||||
from azure.search.documents.indexes.models import (
|
||||
ComplexField,
|
||||
HnswAlgorithmConfiguration,
|
||||
SearchField,
|
||||
SearchFieldDataType,
|
||||
SearchIndex,
|
||||
VectorSearch,
|
||||
VectorSearchProfile,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
|
||||
|
||||
"""
|
||||
The data model used for this sample is based on the hotel data model from the Azure AI Search samples.
|
||||
The source can be found here: https://github.com/Azure/azure-search-vector-samples/blob/main/data/hotels.json
|
||||
The version in this folder, is modified to have python style names and no vectors.
|
||||
Below we define a custom index for the hotel data model.
|
||||
The reason for this is that the built-in connector cannot properly handle the complex data types.
|
||||
|
||||
This file is referenced by the two scripts and does not have to be executed directly.
|
||||
The first script (1_interact_with_the_collection.py) will show interacting with the records.
|
||||
The second script (2_use_as_a_plugin.py) will show interacting with the collection as a plugin.
|
||||
"""
|
||||
|
||||
|
||||
class Rooms(BaseModel):
|
||||
Type: str
|
||||
Description: str
|
||||
Description_fr: str
|
||||
BaseRate: float
|
||||
BedOptions: str
|
||||
SleepsCount: int
|
||||
SmokingAllowed: bool
|
||||
Tags: list[str]
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
class Address(BaseModel):
|
||||
StreetAddress: str
|
||||
City: str | None
|
||||
StateProvince: str | None
|
||||
PostalCode: str | None
|
||||
Country: str | None
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
@vectorstoremodel(collection_name="hotel-index")
|
||||
class HotelSampleClass(BaseModel):
|
||||
HotelId: Annotated[str, VectorStoreField("key")]
|
||||
HotelName: Annotated[str | None, VectorStoreField("data")] = None
|
||||
Description: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
|
||||
DescriptionVector: Annotated[list[float] | str | None, VectorStoreField("vector", dimensions=1536)] = None
|
||||
Description_fr: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
|
||||
DescriptionFrVector: Annotated[list[float] | str | None, VectorStoreField("vector", dimensions=1536)] = None
|
||||
Category: Annotated[str, VectorStoreField("data")]
|
||||
Tags: Annotated[list[str], VectorStoreField("data", is_indexed=True)]
|
||||
ParkingIncluded: Annotated[bool | None, VectorStoreField("data")] = None
|
||||
LastRenovationDate: Annotated[str | None, VectorStoreField("data", type=SearchFieldDataType.DateTimeOffset)] = None
|
||||
Rating: Annotated[float, VectorStoreField("data")]
|
||||
Location: Annotated[dict[str, Any], VectorStoreField("data", type=SearchFieldDataType.GeographyPoint)]
|
||||
Address: Annotated[Address, VectorStoreField("data")]
|
||||
Rooms: Annotated[list[Rooms], VectorStoreField("data")]
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
def model_post_init(self, context: Any) -> None:
|
||||
if self.DescriptionVector is None:
|
||||
self.DescriptionVector = self.Description
|
||||
if self.DescriptionFrVector is None:
|
||||
self.DescriptionFrVector = self.Description_fr
|
||||
|
||||
|
||||
def load_records(url: str | None = None) -> list[HotelSampleClass]:
|
||||
"""
|
||||
Load the records from the given URL (default: Azure hotels.json).
|
||||
Removes the 'DescriptionEmbedding' field.
|
||||
:param url: The URL to the hotels.json file.
|
||||
:return: A list of HotelSampleClass objects.
|
||||
"""
|
||||
if url is None:
|
||||
url = "https://raw.githubusercontent.com/Azure/azure-search-vector-samples/refs/heads/main/data/hotels.json"
|
||||
response = requests.get(url, timeout=60)
|
||||
response.raise_for_status()
|
||||
all_records = response.json()
|
||||
return [HotelSampleClass.model_validate(record) for record in all_records]
|
||||
|
||||
|
||||
custom_index = SearchIndex(
|
||||
name="hotel-index",
|
||||
fields=[
|
||||
SearchField(
|
||||
name="HotelId",
|
||||
type="Edm.String",
|
||||
key=True,
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="HotelName",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=True,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="Description",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=False,
|
||||
sortable=False,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="DescriptionVector",
|
||||
type="Collection(Edm.Single)",
|
||||
hidden=False,
|
||||
searchable=True,
|
||||
vector_search_dimensions=1536,
|
||||
vector_search_profile_name="hnsw",
|
||||
),
|
||||
SearchField(
|
||||
name="Description_fr",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=False,
|
||||
sortable=False,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
analyzer_name="fr.microsoft",
|
||||
),
|
||||
SearchField(
|
||||
name="DescriptionFrVector",
|
||||
type="Collection(Edm.Single)",
|
||||
hidden=False,
|
||||
searchable=True,
|
||||
vector_search_dimensions=1536,
|
||||
vector_search_profile_name="hnsw",
|
||||
),
|
||||
SearchField(
|
||||
name="Category",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="Tags",
|
||||
type="Collection(Edm.String)",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="ParkingIncluded",
|
||||
type="Edm.Boolean",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=False,
|
||||
),
|
||||
SearchField(
|
||||
name="LastRenovationDate",
|
||||
type="Edm.DateTimeOffset",
|
||||
hidden=False,
|
||||
filterable=False,
|
||||
sortable=True,
|
||||
facetable=False,
|
||||
searchable=False,
|
||||
),
|
||||
SearchField(
|
||||
name="Rating",
|
||||
type="Edm.Double",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=True,
|
||||
facetable=True,
|
||||
searchable=False,
|
||||
),
|
||||
ComplexField(
|
||||
name="Address",
|
||||
collection=False,
|
||||
fields=[
|
||||
SearchField(
|
||||
name="StreetAddress",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="City",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="StateProvince",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="PostalCode",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="Country",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
SearchField(
|
||||
name="Location",
|
||||
type="Edm.GeographyPoint",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=True,
|
||||
facetable=False,
|
||||
searchable=False,
|
||||
),
|
||||
ComplexField(
|
||||
name="Rooms",
|
||||
collection=True,
|
||||
fields=[
|
||||
SearchField(
|
||||
name="Description",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=False,
|
||||
sortable=False,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="Description_fr",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=False,
|
||||
sortable=False,
|
||||
facetable=False,
|
||||
searchable=True,
|
||||
analyzer_name="fr.microsoft",
|
||||
),
|
||||
SearchField(
|
||||
name="Type",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="BaseRate",
|
||||
type="Edm.Double",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=False,
|
||||
),
|
||||
SearchField(
|
||||
name="BedOptions",
|
||||
type="Edm.String",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
SearchField(
|
||||
name="SleepsCount",
|
||||
type="Edm.Int64",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=False,
|
||||
),
|
||||
SearchField(
|
||||
name="SmokingAllowed",
|
||||
type="Edm.Boolean",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=False,
|
||||
),
|
||||
SearchField(
|
||||
name="Tags",
|
||||
type="Collection(Edm.String)",
|
||||
hidden=False,
|
||||
filterable=True,
|
||||
sortable=False,
|
||||
facetable=True,
|
||||
searchable=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
vector_search=VectorSearch(
|
||||
profiles=[VectorSearchProfile(name="hnsw", algorithm_configuration_name="hnsw")],
|
||||
algorithms=[HnswAlgorithmConfiguration(name="hnsw")],
|
||||
vectorizers=[],
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,252 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated
|
||||
from uuid import uuid4
|
||||
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
from samples.concepts.memory.utils import print_record
|
||||
from samples.concepts.resources.utils import Colors, print_with_color
|
||||
from semantic_kernel import Kernel
|
||||
from semantic_kernel.connectors.ai.open_ai import AzureTextEmbedding, OpenAITextEmbedding
|
||||
from semantic_kernel.connectors.memory import (
|
||||
AzureAISearchCollection,
|
||||
ChromaCollection,
|
||||
CosmosMongoCollection,
|
||||
CosmosNoSqlCollection,
|
||||
FaissCollection,
|
||||
InMemoryCollection,
|
||||
PineconeCollection,
|
||||
PostgresCollection,
|
||||
QdrantCollection,
|
||||
RedisHashsetCollection,
|
||||
RedisJsonCollection,
|
||||
SqlServerCollection,
|
||||
WeaviateCollection,
|
||||
)
|
||||
from semantic_kernel.data.vector import (
|
||||
SearchType,
|
||||
VectorSearchProtocol,
|
||||
VectorStoreCollection,
|
||||
VectorStoreField,
|
||||
vectorstoremodel,
|
||||
)
|
||||
|
||||
# This is a rather complex sample, showing how to use the vector store
|
||||
# with a number of different collections.
|
||||
# It also shows how to use the vector store with a number of different data models.
|
||||
# It also uses all the types of search available in the vector store.
|
||||
# For a simpler example, see "simple_memory.py"
|
||||
|
||||
|
||||
# Depending on the vector database, the index kind and distance function may need to be adjusted
|
||||
# since not all combinations are supported by all databases.
|
||||
# The values below might need to be changed for your collection to work.
|
||||
@vectorstoremodel(collection_name="test")
|
||||
@dataclass
|
||||
class DataModel:
|
||||
title: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
|
||||
content: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
|
||||
embedding: Annotated[
|
||||
list[float] | str | None,
|
||||
VectorStoreField("vector", dimensions=1536, type="float"),
|
||||
] = None
|
||||
id: Annotated[
|
||||
str,
|
||||
VectorStoreField(
|
||||
"key",
|
||||
),
|
||||
] = field(default_factory=lambda: str(uuid4()))
|
||||
tag: Annotated[str | None, VectorStoreField("data", type="str", is_indexed=True)] = None
|
||||
|
||||
def __post_init__(self, **kwargs):
|
||||
if self.embedding is None:
|
||||
self.embedding = f"{self.title} {self.content}"
|
||||
if self.tag is None:
|
||||
self.tag = "general"
|
||||
|
||||
|
||||
# A list of VectorStoreRecordCollection that can be used.
|
||||
# Available collections are:
|
||||
# - ai_search: Azure AI Search
|
||||
# - postgres: PostgreSQL
|
||||
# - redis_json: Redis JSON
|
||||
# - redis_hashset: Redis Hashset
|
||||
# - qdrant: Qdrant
|
||||
# - in_memory: In-memory store
|
||||
# - weaviate: Weaviate
|
||||
# Please either configure the weaviate settings via environment variables or provide them through the constructor.
|
||||
# Note that embed mode is not supported on Windows: https://github.com/weaviate/weaviate/issues/3315
|
||||
# - azure_cosmos_nosql: Azure Cosmos NoSQL
|
||||
# https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-create-account?tabs=azure-portal
|
||||
# Please see the link above to learn how to set up an Azure Cosmos NoSQL account.
|
||||
# https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-develop-emulator?tabs=windows%2Cpython&pivots=api-nosql
|
||||
# Please see the link above to learn how to set up the Azure Cosmos NoSQL emulator on your machine.
|
||||
# For this sample to work with Azure Cosmos NoSQL, please adjust the index_kind of the data model to QUANTIZED_FLAT.
|
||||
# - azure_cosmos_mongodb: Azure Cosmos MongoDB
|
||||
# https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/introduction
|
||||
# - chroma: Chroma
|
||||
# The chroma collection is currently only available for in-memory versions
|
||||
# Client-Server mode and Chroma Cloud are not yet supported.
|
||||
# More info on Chroma here: https://docs.trychroma.com/docs/overview/introduction
|
||||
# - faiss: Faiss - in-memory with optimized indexes.
|
||||
# - pinecone: Pinecone
|
||||
# - sql_server: SQL Server, can connect to any SQL Server compatible database, like Azure SQL.
|
||||
# This is represented as a mapping from the collection name to a
|
||||
# function which returns the collection.
|
||||
# Using a function allows for lazy initialization of the collection,
|
||||
# so that settings for unused collections do not cause validation errors.
|
||||
collections: dict[str, Callable[[], VectorStoreCollection]] = {
|
||||
"ai_search": lambda: AzureAISearchCollection[str, DataModel](record_type=DataModel),
|
||||
"postgres": lambda: PostgresCollection[str, DataModel](record_type=DataModel),
|
||||
"redis_json": lambda: RedisJsonCollection[str, DataModel](
|
||||
record_type=DataModel,
|
||||
prefix_collection_name_to_key_names=True,
|
||||
),
|
||||
"redis_hash": lambda: RedisHashsetCollection[str, DataModel](
|
||||
record_type=DataModel,
|
||||
prefix_collection_name_to_key_names=True,
|
||||
),
|
||||
"qdrant": lambda: QdrantCollection[str, DataModel](
|
||||
record_type=DataModel,
|
||||
prefer_grpc=True,
|
||||
named_vectors=False,
|
||||
),
|
||||
"in_memory": lambda: InMemoryCollection[str, DataModel](record_type=DataModel),
|
||||
"weaviate": lambda: WeaviateCollection[str, DataModel](record_type=DataModel),
|
||||
"azure_cosmos_nosql": lambda: CosmosNoSqlCollection[str, DataModel](
|
||||
record_type=DataModel,
|
||||
create_database=True,
|
||||
),
|
||||
"azure_cosmos_mongodb": lambda: CosmosMongoCollection[str, DataModel](record_type=DataModel),
|
||||
"faiss": lambda: FaissCollection[str, DataModel](record_type=DataModel),
|
||||
"chroma": lambda: ChromaCollection[str, DataModel](record_type=DataModel),
|
||||
"pinecone": lambda: PineconeCollection[str, DataModel](record_type=DataModel),
|
||||
"sql_server": lambda: SqlServerCollection[str, DataModel](record_type=DataModel),
|
||||
}
|
||||
|
||||
|
||||
async def cleanup(record_collection):
|
||||
print("-" * 30)
|
||||
delete = input("Do you want to delete the collection? (y/n): ")
|
||||
if delete.lower() != "y":
|
||||
print("Skipping deletion.")
|
||||
return
|
||||
print_with_color("Deleting collection!", Colors.CBLUE)
|
||||
await record_collection.ensure_collection_deleted()
|
||||
print_with_color("Done!", Colors.CGREY)
|
||||
|
||||
|
||||
async def main(collection: str, use_azure_openai: bool):
|
||||
print("-" * 30)
|
||||
kernel = Kernel()
|
||||
embedder = (
|
||||
AzureTextEmbedding(service_id="embedding", credential=AzureCliCredential())
|
||||
if use_azure_openai
|
||||
else OpenAITextEmbedding(service_id="embedding")
|
||||
)
|
||||
kernel.add_service(embedder)
|
||||
async with collections[collection]() as record_collection:
|
||||
assert isinstance(record_collection, VectorSearchProtocol) # nosec
|
||||
record_collection.embedding_generator = embedder
|
||||
print_with_color(f"Creating {collection} collection!", Colors.CGREY)
|
||||
# cleanup any existing collection
|
||||
await record_collection.ensure_collection_deleted()
|
||||
# create a new collection
|
||||
await record_collection.ensure_collection_exists()
|
||||
|
||||
record1 = DataModel(
|
||||
content="Semantic Kernel is awesome",
|
||||
id="e6103c03-487f-4d7d-9c23-4723651c17f4",
|
||||
title="Overview",
|
||||
)
|
||||
record2 = DataModel(
|
||||
content="Semantic Kernel is available in dotnet, python and Java.",
|
||||
id="09caec77-f7e1-466a-bcec-f1d51c5b15be",
|
||||
title="Semantic Kernel Languages",
|
||||
)
|
||||
record3 = DataModel(
|
||||
content="```python\nfrom semantic_kernel import Kernel\nkernel = Kernel()\n```",
|
||||
id="d5c9913a-e015-4944-b960-5d4a84bca002",
|
||||
title="Code sample",
|
||||
tag="code",
|
||||
)
|
||||
|
||||
print_with_color("Adding records!", Colors.CBLUE)
|
||||
records = [record1, record2, record3]
|
||||
keys = await record_collection.upsert(records)
|
||||
print(f" Upserted {keys=}")
|
||||
print_with_color("Getting records!", Colors.CBLUE)
|
||||
results = await record_collection.get(top=10, order_by="content")
|
||||
if results:
|
||||
[print_record(record=result) for result in results]
|
||||
else:
|
||||
print("Nothing found...")
|
||||
options = {
|
||||
"vector_property_name": "embedding",
|
||||
"additional_property_name": "content",
|
||||
"filter": lambda x: x.tag == "general",
|
||||
}
|
||||
|
||||
print("-" * 30)
|
||||
print_with_color("Now we can start searching.", Colors.CBLUE)
|
||||
print_with_color(" For each type of search, enter a search term, for instance `python`.", Colors.CBLUE)
|
||||
print_with_color(" Enter exit to exit, and skip or nothing to skip this search.", Colors.CBLUE)
|
||||
print("-" * 30)
|
||||
print_with_color(
|
||||
"This collection supports the following search types: "
|
||||
f"{', '.join(search.value for search in record_collection.supported_search_types)}",
|
||||
Colors.CBLUE,
|
||||
)
|
||||
if SearchType.KEYWORD_HYBRID in record_collection.supported_search_types:
|
||||
search_text = input("Enter search text for hybrid text search: ")
|
||||
if search_text.lower() == "exit":
|
||||
await cleanup(record_collection)
|
||||
return
|
||||
if search_text and search_text.lower() != "skip":
|
||||
print("-" * 30)
|
||||
print_with_color(
|
||||
"Using hybrid text search: ",
|
||||
Colors.CBLUE,
|
||||
)
|
||||
search_results = await record_collection.hybrid_search(values=search_text, **options)
|
||||
if search_results.total_count == 0:
|
||||
print("\nNothing found...\n")
|
||||
else:
|
||||
[print_record(result) async for result in search_results.results]
|
||||
print("-" * 30)
|
||||
|
||||
if SearchType.VECTOR in record_collection.supported_search_types:
|
||||
search_text = input("Enter search text for vector search: ")
|
||||
if search_text.lower() == "exit":
|
||||
await cleanup(record_collection)
|
||||
return
|
||||
if search_text and search_text.lower() != "skip":
|
||||
print("-" * 30)
|
||||
print_with_color(
|
||||
"Using vector search: ",
|
||||
Colors.CBLUE,
|
||||
)
|
||||
search_results = await record_collection.search(search_text, **options)
|
||||
if search_results.total_count == 0:
|
||||
print("\nNothing found...\n")
|
||||
else:
|
||||
[print_record(result) async for result in search_results.results]
|
||||
print("-" * 30)
|
||||
await cleanup(record_collection)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
argparse.ArgumentParser()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--collection", default="in_memory", choices=collections.keys(), help="What collection to use.")
|
||||
# Option of whether to use OpenAI or Azure OpenAI.
|
||||
parser.add_argument("--use-azure-openai", action="store_true", help="Use Azure OpenAI instead of OpenAI.")
|
||||
args = parser.parse_args()
|
||||
args.collection = "ai_search"
|
||||
asyncio.run(main(collection=args.collection, use_azure_openai=args.use_azure_openai))
|
||||
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated, Any
|
||||
from uuid import uuid4
|
||||
|
||||
from pandas import DataFrame
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from semantic_kernel.data.vector import VectorStoreCollectionDefinition, VectorStoreField, vectorstoremodel
|
||||
|
||||
# This concept shows the different ways you can create a vector store data model
|
||||
# using dataclasses, Pydantic, and Python classes.
|
||||
# As well as using types like Pandas Dataframes.
|
||||
|
||||
# There are a number of universal things about these data models:
|
||||
# they must specify the type of field through the annotation (or the definition).
|
||||
# there must be at least one field of type `key`.
|
||||
# A unannotated field is allowed but must have a default value.
|
||||
|
||||
# The purpose of these models is to be what you pass to and get back from a vector store.
|
||||
# There maybe limitations to data types that the vector store can handle,
|
||||
# so not every store will be able to handle completely the same model.
|
||||
# for instance, some stores only allow a string as the keyfield, while others allow str and int,
|
||||
# so defining the key with a int, might make some stores unusable.
|
||||
|
||||
# The decorator takes the class and pulls out the fields and annotations to create a definition,
|
||||
# of type VectorStoreCollectionDefinition.
|
||||
# This definition is used for the vector store to know how to handle the data model.
|
||||
|
||||
# You can also create the definition yourself, and pass it to the vector stores together with a standard type,
|
||||
# like a dict or list.
|
||||
# Or you can use the definition in container mode with something like a Pandas Dataframe.
|
||||
|
||||
|
||||
# Data model using built-in Python dataclasses
|
||||
@vectorstoremodel
|
||||
@dataclass
|
||||
class DataModelDataclass:
|
||||
vector: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None
|
||||
key: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
|
||||
content: Annotated[str, VectorStoreField("data")] = "content1"
|
||||
other: str | None = None
|
||||
|
||||
|
||||
# Data model using Pydantic BaseModels
|
||||
@vectorstoremodel
|
||||
class DataModelPydantic(BaseModel):
|
||||
id: Annotated[str, VectorStoreField("key")] = Field(default_factory=lambda: str(uuid4()))
|
||||
content: Annotated[str, VectorStoreField("data")] = "content1"
|
||||
vector: Annotated[list[float] | None, VectorStoreField("vector", dimensions=3)] = None
|
||||
other: str | None = None
|
||||
|
||||
|
||||
# Data model using Python classes
|
||||
# This one includes a custom serialize and deserialize method
|
||||
@vectorstoremodel
|
||||
class DataModelPython:
|
||||
def __init__(
|
||||
self,
|
||||
key: Annotated[str | None, VectorStoreField("key")] = None,
|
||||
vector: Annotated[list[float], VectorStoreField("vector", dimensions=3)] = None,
|
||||
content: Annotated[str, VectorStoreField("data")] = "content1",
|
||||
other: str | None = None,
|
||||
):
|
||||
self.vector = vector
|
||||
self.other = other
|
||||
self.key = key or str(uuid4())
|
||||
self.content = content
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"DataModelPython(vector={self.vector}, key={self.key}, content={self.content}, other={self.other})"
|
||||
|
||||
def serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"vector": self.vector,
|
||||
"key": self.key,
|
||||
"content": self.content,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, obj: dict[str, Any]) -> "DataModelPython":
|
||||
return cls(
|
||||
vector=obj["vector"],
|
||||
key=obj["key"],
|
||||
content=obj["content"],
|
||||
)
|
||||
|
||||
|
||||
# Data model definition for use with Pandas
|
||||
# note the container mode flag, which makes sure that records that are returned are in a container
|
||||
# even when requesting a batch of records.
|
||||
# There is also a to_dict and from_dict method, which are used to convert the data model to and from a dict,
|
||||
# these should be specific to the type used, if using dict as type then these can be left off.
|
||||
definition_pandas = VectorStoreCollectionDefinition(
|
||||
fields=[
|
||||
VectorStoreField("vector", name="vector", type="float", dimensions=3),
|
||||
VectorStoreField("key", name="key", type="str"),
|
||||
VectorStoreField("data", name="content", type="str"),
|
||||
],
|
||||
container_mode=True,
|
||||
to_dict=lambda record, **_: record.to_dict(orient="records"),
|
||||
from_dict=lambda records, **_: DataFrame(records),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
data_item1 = DataModelDataclass(content="Hello, world!", vector=[1.0, 2.0, 3.0], other=None)
|
||||
data_item2 = DataModelPydantic(content="Hello, world!", vector=[1.0, 2.0, 3.0], other=None)
|
||||
data_item3 = DataModelPython(content="Hello, world!", vector=[1.0, 2.0, 3.0], other=None)
|
||||
print("Example records:")
|
||||
print(f"DataClass:\n {data_item1}", end="\n\n")
|
||||
print(f"Pydantic:\n {data_item2}", end="\n\n")
|
||||
print(f"Python:\n {data_item3}", end="\n\n")
|
||||
|
||||
print("Item definitions:")
|
||||
print(f"DataClass:\n {data_item1.__kernel_vectorstoremodel_definition__}", end="\n\n")
|
||||
print(f"Pydantic:\n {data_item2.__kernel_vectorstoremodel_definition__}", end="\n\n")
|
||||
print(f"Python:\n {data_item3.__kernel_vectorstoremodel_definition__}", end="\n\n")
|
||||
print(f"Definition for use with Pandas:\n {definition_pandas}", end="\n\n")
|
||||
if (
|
||||
data_item1.__kernel_vectorstoremodel_definition__.fields
|
||||
== data_item2.__kernel_vectorstoremodel_definition__.fields
|
||||
== data_item3.__kernel_vectorstoremodel_definition__.fields
|
||||
== definition_pandas.fields
|
||||
):
|
||||
print("All data models are the same")
|
||||
else:
|
||||
print("Data models are not the same")
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from uuid import uuid4
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding
|
||||
from semantic_kernel.connectors.azure_ai_search import AzureAISearchCollection
|
||||
from semantic_kernel.data.vector import VectorStoreCollectionDefinition, VectorStoreField
|
||||
|
||||
definition = VectorStoreCollectionDefinition(
|
||||
collection_name="pandas_test_index",
|
||||
fields=[
|
||||
VectorStoreField("key", name="id", type="str"),
|
||||
VectorStoreField("data", name="title", type="str"),
|
||||
VectorStoreField("data", name="content", type="str", is_full_text_indexed=True),
|
||||
VectorStoreField(
|
||||
"vector",
|
||||
name="vector",
|
||||
type="float",
|
||||
dimensions=1536,
|
||||
embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"),
|
||||
),
|
||||
],
|
||||
to_dict=lambda record, **_: record.to_dict(orient="records"),
|
||||
from_dict=lambda records, **_: pd.DataFrame(records),
|
||||
container_mode=True,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
# create the record collection
|
||||
async with AzureAISearchCollection[str, pd.DataFrame](
|
||||
record_type=pd.DataFrame,
|
||||
definition=definition,
|
||||
) as collection:
|
||||
await collection.ensure_collection_exists()
|
||||
# create some records
|
||||
records = [
|
||||
{
|
||||
"id": str(uuid4()),
|
||||
"title": "Document about Semantic Kernel.",
|
||||
"content": "Semantic Kernel is a framework for building AI applications.",
|
||||
},
|
||||
{
|
||||
"id": str(uuid4()),
|
||||
"title": "Document about Python",
|
||||
"content": "Python is a programming language that lets you work quickly.",
|
||||
},
|
||||
]
|
||||
|
||||
# create the dataframe and add the content you want to embed to a new column
|
||||
df = pd.DataFrame(records)
|
||||
df["vector"] = df.apply(lambda row: f"title: {row['title']}, content: {row['content']}", axis=1)
|
||||
print(df.head(1))
|
||||
# upsert the records (for a container, upsert and upsert_batch are equivalent)
|
||||
await collection.upsert(df)
|
||||
|
||||
# retrieve a record
|
||||
result = await collection.get(top=2)
|
||||
if result is None:
|
||||
print("No records found, this is sometimes because the get is too fast and the index is not ready yet.")
|
||||
else:
|
||||
print("Retrieved records:")
|
||||
print(result.to_string())
|
||||
|
||||
await collection.ensure_collection_deleted()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,131 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Annotated
|
||||
from uuid import uuid4
|
||||
|
||||
from samples.concepts.memory.utils import print_record
|
||||
from samples.concepts.resources.utils import Colors, print_with_color
|
||||
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding
|
||||
from semantic_kernel.connectors.in_memory import InMemoryCollection
|
||||
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
|
||||
|
||||
# This is the most basic example of a vector store and collection
|
||||
# For a more complex example, using different collection types, see "complex_memory.py"
|
||||
# This sample uses openai text embeddings, so make sure to have your environment variables set up
|
||||
# it needs openai api key and embedding model id
|
||||
embedder = OpenAITextEmbedding(service_id="embedding")
|
||||
|
||||
# Next, you need to define your data structure
|
||||
# In this case, we are using a dataclass to define our data structure
|
||||
# you can also use a pydantic model, or a vanilla python class, see "data_models.py" for more examples
|
||||
# Inside the model we define which fields we want to use, and which fields are vectors
|
||||
# and for vector fields we define what kind of index we want to use, and what distance function we want to use
|
||||
# This has been done in constants here for simplicity, but you can also define them in the model itself
|
||||
# Next we create three records using that model
|
||||
|
||||
|
||||
@vectorstoremodel(collection_name="test")
|
||||
@dataclass
|
||||
class DataModel:
|
||||
content: Annotated[str, VectorStoreField("data")]
|
||||
id: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
|
||||
vector: Annotated[
|
||||
list[float] | str | None,
|
||||
VectorStoreField("vector", dimensions=1536),
|
||||
] = None
|
||||
title: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)] = "title"
|
||||
tag: Annotated[str, VectorStoreField("data", is_indexed=True)] = "tag"
|
||||
|
||||
def __post_init__(self):
|
||||
if self.vector is None:
|
||||
self.vector = self.content
|
||||
|
||||
|
||||
records = [
|
||||
DataModel(
|
||||
content="Semantic Kernel is awesome",
|
||||
id="e6103c03-487f-4d7d-9c23-4723651c17f4",
|
||||
title="Overview",
|
||||
tag="general",
|
||||
),
|
||||
DataModel(
|
||||
content="Semantic Kernel is available in dotnet, python and Java.",
|
||||
id="09caec77-f7e1-466a-bcec-f1d51c5b15be",
|
||||
title="Semantic Kernel Languages",
|
||||
tag="general",
|
||||
),
|
||||
DataModel(
|
||||
content="```python\nfrom semantic_kernel import Kernel\nkernel = Kernel()\n```",
|
||||
id="d5c9913a-e015-4944-b960-5d4a84bca002",
|
||||
title="Code sample",
|
||||
tag="code",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def main():
|
||||
print("-" * 30)
|
||||
# Create the collection here
|
||||
# by using the generic we make sure that IDE's understand what you need to pass in and get back
|
||||
# we also use the async with to open and close the connection
|
||||
# for the in memory collection, this is just a no-op
|
||||
# but for other collections, like Azure AI Search, this will open and close the connection
|
||||
async with InMemoryCollection[str, DataModel](
|
||||
record_type=DataModel,
|
||||
embedding_generator=embedder,
|
||||
) as record_collection:
|
||||
# Create the collection after wiping it
|
||||
print_with_color("Creating test collection!", Colors.CGREY)
|
||||
await record_collection.ensure_collection_exists()
|
||||
|
||||
# First add vectors to the records
|
||||
print_with_color("Adding records!", Colors.CBLUE)
|
||||
keys = await record_collection.upsert(records)
|
||||
print(f" Upserted {keys=}")
|
||||
print("-" * 30)
|
||||
|
||||
# Now we can get the records back
|
||||
print_with_color("Getting records!", Colors.CBLUE)
|
||||
results = await record_collection.get([records[0].id, records[1].id, records[2].id])
|
||||
if results and isinstance(results, Sequence):
|
||||
[print_record(record=result) for result in results]
|
||||
else:
|
||||
print("Nothing found...")
|
||||
print("-" * 30)
|
||||
|
||||
# Now we can search for records
|
||||
# First we define the options
|
||||
# The most important option is the vector_field_name, which is the name of the field that contains the vector
|
||||
# The other options are optional, but can be useful
|
||||
# The filter option is used to filter the results based on the tag field
|
||||
options = {
|
||||
"vector_property_name": "vector",
|
||||
"filter": lambda x: x.tag == "general",
|
||||
}
|
||||
query = "python"
|
||||
print_with_color(f"Searching for '{query}', with filter 'tag == general'", Colors.CBLUE)
|
||||
print_with_color(
|
||||
"Using vectorized search, the lower the score the better",
|
||||
Colors.CBLUE,
|
||||
)
|
||||
search_results = await record_collection.search(
|
||||
values=query,
|
||||
**options,
|
||||
)
|
||||
if search_results.total_count == 0:
|
||||
print("\nNothing found...\n")
|
||||
else:
|
||||
[print_record(result) async for result in search_results.results]
|
||||
print("-" * 30)
|
||||
|
||||
# lets cleanup!
|
||||
print_with_color("Deleting collection!", Colors.CBLUE)
|
||||
await record_collection.ensure_collection_deleted()
|
||||
print_with_color("Done!", Colors.CGREY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
from samples.concepts.resources.utils import Colors, print_with_color
|
||||
from semantic_kernel.data.vector import VectorSearchResult
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def print_record(result: VectorSearchResult[_T] | None = None, record: _T | None = None):
|
||||
if result:
|
||||
record = result.record
|
||||
print_with_color(f" Found id: {record.id}", Colors.CGREEN)
|
||||
if result and result.score is not None:
|
||||
print_with_color(f" Score: {result.score}", Colors.CWHITE)
|
||||
print_with_color(f" Title: {record.title}", Colors.CWHITE)
|
||||
print_with_color(f" Content: {record.content}", Colors.CWHITE)
|
||||
print_with_color(f" Tag: {record.tag}", Colors.CWHITE)
|
||||
Reference in New Issue
Block a user