Skip to content

Commit 003cc4c

Browse files
Python: Feature python vector stores preb (#12271)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Overhaul of the VectorStores to get it matching with dotnet and the latest development there. Closes: - #11938 - #11598 - #11597 - #11517 - #10561 - #10391 - #9911 - #9892 - #10867 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Many changes, some highlights: - Moved from different vector fields types, to a single with a enum first VectorStoreField() - Renamed several items - Made embeddinggenerrator a part of the vector field spec and do automatic vectorization from there or from the same param on a collection - Adds hybrid search using the same setup - Removed the intermediate TextSeachVectorSearch object need, by allowing `create_search_function` directly on a collection Will write out full changes in a migration guide in the docs. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
1 parent 466a610 commit 003cc4c

File tree

258 files changed

+15398
-19214
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

258 files changed

+15398
-19214
lines changed

python/.coveragerc

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
11
[run]
22
source = semantic_kernel
33
omit =
4-
semantic_kernel/connectors/memory/astradb/*
5-
semantic_kernel/connectors/memory/azure_cognitive_search/*
6-
semantic_kernel/connectors/memory/azure_cosmosdb/*
7-
semantic_kernel/connectors/memory/azure_cosmosdb_no_sql/*
8-
semantic_kernel/connectors/memory/chroma/chroma_memory_store.py
9-
semantic_kernel/connectors/memory/milvus/*
10-
semantic_kernel/connectors/memory/mongodb_atlas/mongodb_atlas_memory_store.py
11-
semantic_kernel/connectors/memory/pinecone/pinecone_memory_store.py
12-
semantic_kernel/connectors/memory/pinecone/utils.py
13-
semantic_kernel/connectors/memory/postgres/postgres_memory_store.py
14-
semantic_kernel/connectors/memory/qdrant/qdrant_memory_store.py
15-
semantic_kernel/connectors/memory/redis/redis_memory_store.py
16-
semantic_kernel/connectors/memory/usearch/*
17-
semantic_kernel/connectors/memory/weaviate/weaviate_memory_store.py
4+
semantic_kernel/connectors/memory_stores/*
185
semantic_kernel/reliability/*
196
semantic_kernel/memory/*
207

python/mypy.ini

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,50 +17,9 @@ disable_error_code = method-assign
1717

1818
[mypy-semantic_kernel.memory.*]
1919
ignore_errors = true
20-
# TODO (eavanvalkenburg): remove this
21-
# https://github.com/microsoft/semantic-kernel/issues/6463
20+
# TODO (eavanvalkenburg): remove this when removing the memory stores
2221

23-
[mypy-semantic_kernel.connectors.memory.astradb.*]
22+
[mypy-semantic_kernel.connectors.memory_stores.*]
2423
ignore_errors = true
24+
# TODO (eavanvalkenburg): remove this when removing the memory stores
2525

26-
[mypy-semantic_kernel.connectors.memory.azure_ai_search.*]
27-
ignore_errors = false
28-
[mypy-semantic_kernel.connectors.memory.azure_cognitive_search.*]
29-
ignore_errors = true
30-
31-
[mypy-semantic_kernel.connectors.memory.azure_cosmosdb.*]
32-
ignore_errors = true
33-
34-
[mypy-semantic_kernel.connectors.memory.azure_cosmosdb_no_sql.*]
35-
ignore_errors = true
36-
37-
[mypy-semantic_kernel.connectors.memory.chroma.*]
38-
ignore_errors = true
39-
40-
[mypy-semantic_kernel.connectors.memory.milvus.*]
41-
ignore_errors = true
42-
43-
[mypy-semantic_kernel.connectors.memory.mongodb_atlas.*]
44-
ignore_errors = true
45-
46-
[mypy-semantic_kernel.connectors.memory.pinecone.pinecone_memory_store]
47-
ignore_errors = true
48-
49-
[mypy-semantic_kernel.connectors.memory.postgres.*]
50-
ignore_errors = true
51-
52-
[mypy-semantic_kernel.connectors.memory.qdrant.qdrant_vector_record_store.*]
53-
ignore_errors = true
54-
[mypy-semantic_kernel.connectors.memory.qdrant.*]
55-
ignore_errors = true
56-
57-
[mypy-semantic_kernel.connectors.memory.redis.redis_vector_record_store.*]
58-
ignore_errors = true
59-
[mypy-semantic_kernel.connectors.memory.redis.*]
60-
ignore_errors = true
61-
62-
[mypy-semantic_kernel.connectors.memory.usearch.*]
63-
ignore_errors = true
64-
65-
[mypy-semantic_kernel.connectors.memory.weaviate.*]
66-
ignore_errors = true

python/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ pandas = [
123123
"pandas ~= 2.2"
124124
]
125125
pinecone = [
126-
"pinecone[asyncio, grpc] ~= 6.0"
126+
"pinecone[asyncio, grpc] ~= 7.0"
127127
]
128128
postgres = [
129129
"psycopg[binary,pool] ~= 3.2"
@@ -132,7 +132,7 @@ qdrant = [
132132
"qdrant-client ~= 1.9"
133133
]
134134
redis = [
135-
"redis[hiredis] >= 5,< 6",
135+
"redis[hiredis] ~= 6.0",
136136
"types-redis ~= 4.6.0.20240425",
137137
"redisvl ~= 0.4"
138138
]
@@ -215,6 +215,7 @@ select = [
215215
ignore = [
216216
"D100", #allow missing docstring in public module
217217
"D104", #allow missing docstring in public package
218+
"D418", #allow docstring on overloaded function
218219
"TD003", #allow missing link to todo issue
219220
"FIX002" #allow todo
220221
]

python/samples/concepts/caching/semantic_caching.py

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,25 @@
88
from uuid import uuid4
99

1010
from semantic_kernel import Kernel
11-
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
1211
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion, OpenAITextEmbedding
13-
from semantic_kernel.connectors.memory.in_memory.in_memory_store import InMemoryVectorStore
14-
from semantic_kernel.data import (
15-
VectorizedSearchMixin,
16-
VectorSearchOptions,
17-
VectorStore,
18-
VectorStoreRecordCollection,
19-
VectorStoreRecordDataField,
20-
VectorStoreRecordKeyField,
21-
VectorStoreRecordVectorField,
22-
vectorstoremodel,
23-
)
12+
from semantic_kernel.connectors.in_memory import InMemoryStore
13+
from semantic_kernel.data.vector import VectorStore, VectorStoreCollection, VectorStoreField, vectorstoremodel
2414
from semantic_kernel.filters import FilterTypes, FunctionInvocationContext, PromptRenderContext
2515
from semantic_kernel.functions import FunctionResult
2616

2717
COLLECTION_NAME = "llm_responses"
2818
RECORD_ID_KEY = "cache_record_id"
2919

3020

31-
# Define a simple data model to store, the prompt, the result, and the prompt embedding.
32-
@vectorstoremodel
21+
# Define a simple data model to store, the prompt and the result
22+
# we annotate the prompt field as the vector field, the prompt itself will not be stored.
23+
# and if you use `include_vectors` in the search, it will return the vector, but not the prompt.
24+
@vectorstoremodel(collection_name=COLLECTION_NAME)
3325
@dataclass
3426
class CacheRecord:
35-
prompt: Annotated[str, VectorStoreRecordDataField(embedding_property_name="prompt_embedding")]
36-
result: Annotated[str, VectorStoreRecordDataField(is_full_text_searchable=True)]
37-
prompt_embedding: Annotated[list[float], VectorStoreRecordVectorField(dimensions=1536)] = field(
38-
default_factory=list
39-
)
40-
id: Annotated[str, VectorStoreRecordKeyField] = field(default_factory=lambda: str(uuid4()))
27+
result: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
28+
prompt: Annotated[str | None, VectorStoreField("vector", dimensions=1536)] = None
29+
id: Annotated[str, VectorStoreField("key")] = field(default_factory=lambda: str(uuid4()))
4130

4231

4332
# Define the filters, one for caching the results and one for using the cache.
@@ -46,16 +35,13 @@ class PromptCacheFilter:
4635

4736
def __init__(
4837
self,
49-
embedding_service: EmbeddingGeneratorBase,
5038
vector_store: VectorStore,
51-
collection_name: str = COLLECTION_NAME,
5239
score_threshold: float = 0.2,
5340
):
54-
self.embedding_service = embedding_service
41+
if vector_store.embedding_generator is None:
42+
raise ValueError("The vector store must have an embedding generator.")
5543
self.vector_store = vector_store
56-
self.collection: VectorStoreRecordCollection[str, CacheRecord] = vector_store.get_collection(
57-
collection_name, data_model_type=CacheRecord
58-
)
44+
self.collection: VectorStoreCollection[str, CacheRecord] = vector_store.get_collection(record_type=CacheRecord)
5945
self.score_threshold = score_threshold
6046

6147
async def on_prompt_render(
@@ -69,15 +55,10 @@ async def on_prompt_render(
6955
closer the match.
7056
"""
7157
await next(context)
72-
assert context.rendered_prompt # nosec
73-
prompt_embedding = await self.embedding_service.generate_raw_embeddings([context.rendered_prompt])
74-
await self.collection.create_collection_if_not_exists()
75-
assert isinstance(self.collection, VectorizedSearchMixin) # nosec
76-
results = await self.collection.vectorized_search(
77-
vector=prompt_embedding[0], options=VectorSearchOptions(vector_field_name="prompt_embedding", top=1)
78-
)
58+
await self.collection.ensure_collection_exists()
59+
results = await self.collection.search(context.rendered_prompt, vector_property_name="prompt", top=1)
7960
async for result in results.results:
80-
if result.score < self.score_threshold:
61+
if result.score and result.score < self.score_threshold:
8162
context.function_result = FunctionResult(
8263
function=context.function.metadata,
8364
value=result.record.result,
@@ -92,13 +73,8 @@ async def on_function_invocation(
9273
await next(context)
9374
result = context.result
9475
if result and result.rendered_prompt and RECORD_ID_KEY not in result.metadata:
95-
prompt_embedding = await self.embedding_service.generate_embeddings([result.rendered_prompt])
96-
cache_record = CacheRecord(
97-
prompt=result.rendered_prompt,
98-
result=str(result),
99-
prompt_embedding=prompt_embedding[0],
100-
)
101-
await self.collection.create_collection_if_not_exists()
76+
cache_record = CacheRecord(prompt=result.rendered_prompt, result=str(result))
77+
await self.collection.ensure_collection_exists()
10278
await self.collection.upsert(cache_record)
10379

10480

@@ -118,11 +94,10 @@ async def main():
11894
chat = OpenAIChatCompletion(service_id="default")
11995
embedding = OpenAITextEmbedding(service_id="embedder")
12096
kernel.add_service(chat)
121-
kernel.add_service(embedding)
12297
# create the in-memory vector store
123-
vector_store = InMemoryVectorStore()
98+
vector_store = InMemoryStore(embedding_generator=embedding)
12499
# create the cache filter and add the filters to the kernel
125-
cache = PromptCacheFilter(embedding_service=embedding, vector_store=vector_store)
100+
cache = PromptCacheFilter(vector_store=vector_store)
126101
kernel.add_filter(FilterTypes.PROMPT_RENDERING, cache.on_prompt_render)
127102
kernel.add_filter(FilterTypes.FUNCTION_INVOCATION, cache.on_function_invocation)
128103

python/samples/concepts/chat_history/store_chat_history_in_cosmosdb.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,11 @@
77
from samples.concepts.setup.chat_completion_services import Services, get_chat_completion_service_and_request_settings
88
from semantic_kernel import Kernel
99
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
10-
from semantic_kernel.connectors.memory.azure_cosmos_db.azure_cosmos_db_no_sql_store import AzureCosmosDBNoSQLStore
10+
from semantic_kernel.connectors.azure_cosmos_db import CosmosNoSqlStore
1111
from semantic_kernel.contents import ChatHistory, ChatMessageContent
1212
from semantic_kernel.core_plugins.math_plugin import MathPlugin
1313
from semantic_kernel.core_plugins.time_plugin import TimePlugin
14-
from semantic_kernel.data import (
15-
VectorStore,
16-
VectorStoreRecordCollection,
17-
VectorStoreRecordDataField,
18-
VectorStoreRecordKeyField,
19-
vectorstoremodel,
20-
)
14+
from semantic_kernel.data.vector import VectorStore, VectorStoreCollection, VectorStoreField, vectorstoremodel
2115

2216
"""
2317
This sample demonstrates how to build a conversational chatbot
@@ -39,9 +33,9 @@
3933
@vectorstoremodel
4034
@dataclass
4135
class ChatHistoryModel:
42-
session_id: Annotated[str, VectorStoreRecordKeyField]
43-
user_id: Annotated[str, VectorStoreRecordDataField(is_filterable=True)]
44-
messages: Annotated[list[dict[str, str]], VectorStoreRecordDataField(is_filterable=True)]
36+
session_id: Annotated[str, VectorStoreField("key")]
37+
user_id: Annotated[str, VectorStoreField("data", is_indexed=True)]
38+
messages: Annotated[list[dict[str, str]], VectorStoreField("data", is_indexed=True)]
4539

4640

4741
# 2. We then create a class that extends the ChatHistory class
@@ -55,7 +49,7 @@ class ChatHistoryInCosmosDB(ChatHistory):
5549
session_id: str
5650
user_id: str
5751
store: VectorStore
58-
collection: VectorStoreRecordCollection[str, ChatHistoryModel] | None = None
52+
collection: VectorStoreCollection[str, ChatHistoryModel] | None = None
5953

6054
async def create_collection(self, collection_name: str) -> None:
6155
"""Create a collection with the inbuild data model using the vector store.
@@ -64,9 +58,9 @@ async def create_collection(self, collection_name: str) -> None:
6458
"""
6559
self.collection = self.store.get_collection(
6660
collection_name=collection_name,
67-
data_model_type=ChatHistoryModel,
61+
record_type=ChatHistoryModel,
6862
)
69-
await self.collection.create_collection_if_not_exists()
63+
await self.collection.ensure_collection_exists()
7064

7165
async def store_messages(self) -> None:
7266
"""Store the chat history in the Cosmos DB.
@@ -175,7 +169,7 @@ async def main() -> None:
175169

176170
# First we enter the store context manager to connect.
177171
# The create_database flag will create the database if it does not exist.
178-
async with AzureCosmosDBNoSQLStore(create_database=True) as store:
172+
async with CosmosNoSqlStore(create_database=True) as store:
179173
# Then we create the chat history in CosmosDB.
180174
history = ChatHistoryInCosmosDB(store=store, session_id=session_id, user_id="user")
181175
# Finally we create the collection.
@@ -191,7 +185,7 @@ async def main() -> None:
191185
except Exception:
192186
print("Closing chat...")
193187
if delete_when_done and history.collection:
194-
await history.collection.delete_collection()
188+
await history.collection.ensure_collection_deleted()
195189

196190

197191
if __name__ == "__main__":
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
5+
from samples.concepts.memory.azure_ai_search_hotel_samples.data_model import (
6+
HotelSampleClass,
7+
custom_index,
8+
load_records,
9+
)
10+
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding
11+
from semantic_kernel.connectors.azure_ai_search import AzureAISearchCollection
12+
13+
"""
14+
With the data model and records defined in data_model.py, this script will create an Azure AI Search collection,
15+
upsert the records, and then search the collection using vector and hybrid search.
16+
The script will print the first five records in the collection and the search results.
17+
The script will also delete the collection at the end.
18+
19+
Note that we add the OpenAITextEmbedding to the collection, which is used to generate the vectors.
20+
To use the built-in embedding in Azure AI Search, remove this and add that definition to the custom_index.
21+
"""
22+
23+
24+
async def main(query: str):
25+
records = load_records()
26+
# Create the Azure AI Search collection
27+
async with AzureAISearchCollection[str, HotelSampleClass](
28+
record_type=HotelSampleClass, embedding_generator=OpenAITextEmbedding()
29+
) as collection:
30+
# Check if the collection exists.
31+
await collection.ensure_collection_exists(index=custom_index)
32+
await collection.upsert(records)
33+
# get the first five records to check the upsert worked.
34+
results = await collection.get(order_by="HotelName", top=5)
35+
print("Get first five records: ")
36+
if results:
37+
for result in results:
38+
print(
39+
f" {result.HotelId} (in {result.Address.City}, {result.Address.Country}): {result.Description}"
40+
)
41+
42+
print("\n")
43+
print("Search results using vector: ")
44+
# Use search to search using the vector.
45+
results = await collection.search(
46+
query,
47+
vector_property_name="DescriptionVector",
48+
)
49+
async for result in results.results:
50+
print(
51+
f" {result.record.HotelId} (in {result.record.Address.City}, "
52+
f"{result.record.Address.Country}): {result.record.Description} (score: {result.score})"
53+
)
54+
print("\n")
55+
print("Search results using hybrid: ")
56+
# Use hybrid search to search using the vector.
57+
results = await collection.hybrid_search(
58+
query,
59+
vector_property_name="DescriptionVector",
60+
additional_property_name="Description",
61+
)
62+
async for result in results.results:
63+
print(
64+
f" {result.record.HotelId} (in {result.record.Address.City}, "
65+
f"{result.record.Address.Country}): {result.record.Description} (score: {result.score})"
66+
)
67+
68+
await collection.ensure_collection_deleted()
69+
70+
71+
if __name__ == "__main__":
72+
query = "swimming pool and good internet connection"
73+
asyncio.run(main(query=query))

0 commit comments

Comments
 (0)