chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -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 Hotels 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())
+19
View File
@@ -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)