Przewodnik migracji repozytorium wektorów Semantic Kernel w Pythonie

Przegląd

W tym przewodniku omówiono główne aktualizacje magazynu wektorów wprowadzone w wersji 1.34 jądra semantycznego, co stanowi znaczący przegląd implementacji magazynu wektorów w celu dostosowania do zestawu .NET SDK i zapewnienia bardziej ujednoliconego, intuicyjnego interfejsu API. Zmiany konsolidują wszystko w obszarze semantic_kernel.data.vector i ulepszają architekturę łącznika.

Podsumowanie najważniejszych ulepszeń

  • Unified Field Model: pojedyncza VectorStoreField klasa zastępuje wiele typów pól
  • Zintegrowane osadzanie: bezpośrednie generowanie osadzania w specyfikacji pola wektorowego
  • Uproszczone wyszukiwanie: łatwe tworzenie funkcji wyszukiwania bezpośrednio w kolekcjach
  • Struktura skonsolidowana: wszystko w obszarze semantic_kernel.data.vector i semantic_kernel.connectors
  • Ulepszone wyszukiwanie tekstu: ulepszone funkcje wyszukiwania tekstu z udoskonalonymi łącznikami
  • Wycofanie: stare memory_stores są przestarzałe na rzecz nowej architektury magazynu wektorów

1. Zintegrowane osadzanie i wektorowe modele/aktualizacje pól

Istnieje wiele zmian w sposobie definiowania modelu magazynu wektorów. Największą zaletą jest to, że obecnie obsługujemy zintegrowane osadzanie bezpośrednio w definicjach pól magazynu wektorów. Oznacza to, że po określeniu pola jako wektora zawartość tego pola jest automatycznie osadzana przy użyciu określonego generatora osadzania, takiego jak model osadzania tekstu openAI. Upraszcza to proces tworzenia pól wektorów i zarządzania nimi.

Podczas definiowania tego pola należy upewnić się, że istnieją trzy elementy, zwłaszcza w przypadku korzystania z modelu Pydantic:

  1. wpisywanie: pole prawdopodobnie będzie miało trzy typy, list[float], str lub coś innego dla danych wejściowych generatora osadzania, a None w przypadku, gdy pole jest niezastawione.
  2. wartość domyślna: pole musi mieć wartość None domyślną lub coś innego, aby nie wystąpił błąd podczas pobierania rekordów z get lub search z include_vectors=False którym jest teraz wartością domyślną.

Są dwa problemy tutaj: po pierwsze, podczas dekorowania klasy z vectorstoremodel, pierwsza adnotacja typu pola jest używana do wypełnienia parametru type klasy VectorStoreField, więc należy upewnić się, że pierwsza adnotacja typu jest właściwym typem dla kolekcji przechowywania wektorów, która ma zostać utworzona, często list[float]. Domyślnie metody get i search nie zawierają wektorów w wynikach, więc pole wymaga wartości domyślnej, a typ musi być z tym zgodny. Dlatego często dozwolone jest None, a wartość domyślna jest ustawiona na None. Po utworzeniu pola wartości, które należy osadzić, znajdują się w tym polu, często ciągi, więc str należy je również uwzględnić. Przyczyną tej zmiany jest umożliwienie większej elastyczności w tym, co jest osadzone i tym, co jest rzeczywiście przechowywane w polach danych. To byłaby typowa konfiguracja:

from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
from typing import Annotated
from dataclasses import dataclass

@vectorstoremodel
@dataclass
class MyRecord:
    content: Annotated[str, VectorStoreField('data', is_indexed=True, is_full_text_indexed=True)]
    title: Annotated[str, VectorStoreField('data', is_indexed=True, is_full_text_indexed=True)]
    id: Annotated[str, VectorStoreField('key')]
    vector: Annotated[list[float] | str | None, VectorStoreField(
        'vector', 
        dimensions=1536, 
        distance_function="cosine",
        embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"),
    )] = None

    def __post_init__(self):
        if self.vector is None:
            self.vector = f"Title: {self.title}, Content: {self.content}"

Zwróć uwagę na metodę post_init. To tworzy wartość, która zostaje osadzona i obejmuje więcej niż jedno pole. Istnieją również trzy typy.

Przed: oddzielne klasy pól

from semantic_kernel.data import (
    VectorStoreRecordKeyField,
    VectorStoreRecordDataField, 
    VectorStoreRecordVectorField
)

# Old approach with separate field classes
fields = [
    VectorStoreRecordKeyField(name="id"),
    VectorStoreRecordDataField(name="text", is_filterable=True, is_full_text_searchable=True),
    VectorStoreRecordVectorField(name="vector", dimensions=1536, distance_function="cosine")
]

Po: Unified VectorStoreField ze zintegrowanymi osadzaniami

from semantic_kernel.data.vector import VectorStoreField
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding

# New unified approach with integrated embeddings
embedding_service = OpenAITextEmbedding(
    ai_model_id="text-embedding-3-small"
)

fields = [
    VectorStoreField(
        "key",
        name="id",
    ),
    VectorStoreField(
        "data",
        name="text",
        is_indexed=True,  # Previously is_filterable
        is_full_text_indexed=True  # Previously is_full_text_searchable
    ),
    VectorStoreField(
        "vector",
        name="vector",
        dimensions=1536,
        distance_function="cosine",
        embedding_generator=embedding_service  # Integrated embedding generation
    )
]

Kluczowe zmiany w definicji pola

  1. Klasa pojedynczego pola: VectorStoreField zastępuje wszystkie poprzednie typy pól
  2. Specyfikacja typu pola: użyj field_type: Literal["key", "data", "vector"] parametru , może to być parametr pozycyjny, więc VectorStoreField("key") jest prawidłowy.
  3. Właściwości rozszerzone:
    • storage_name został dodany i po ustawieniu jest używany jako nazwa pola w magazynie wektorów. W przeciwnym przypadku używany jest parametr name.
    • dimensions parametr jest teraz wymagany dla pól wektorowych.
    • distance_function i index_kind są opcjonalne i zostaną ustawione odpowiednio na DistanceFunction.DEFAULT i IndexKind.DEFAULT, jeśli nie zostaną określone, i tylko dla pól wektorowych. Każda implementacja przechowywania wektorów ma logikę, która wybiera wartość domyślną dla tego przechowywania.
  4. Nazwy właściwości:
    • property_type type_→ jako atrybut i type w konstruktorach
    • is_filterableis_indexed
    • is_full_text_searchableis_full_text_indexed
  5. Zintegrowane osadzanie: dodawanie embedding_generator bezpośrednio do pól wektorowych, alternatywnie można ustawić embedding_generator dla samej kolekcji magazynów wektorów, która będzie używana dla wszystkich pól wektorowych w tym magazynie, ta wartość ma pierwszeństwo przed generatorem osadzania na poziomie kolekcji.

2. Nowe metody w sklepach i kolekcjach

Rozszerzony interfejs magazynu

from semantic_kernel.connectors.in_memory import InMemoryStore

# Before: Limited collection methods
collection = InMemoryStore.get_collection("my_collection", record_type=MyRecord)

# After: Slimmer collection interface with new methods
collection = InMemoryStore.get_collection(MyRecord)
# if the record type has the `vectorstoremodel` decorator it can contain both the collection_name and the definition for the collection.

# New methods for collection management
await store.collection_exists("my_collection")
await store.ensure_collection_deleted("my_collection")
# both of these methods, create a simple model to streamline doing collection management tasks.
# they both call the underlying `VectorStoreCollection` methods, see below.

Ulepszony interfejs kolekcji

from semantic_kernel.connectors.in_memory import InMemoryCollection

collection = InMemoryCollection(
    record_type=MyRecord,
    embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small")  # Optional, if there is no embedding generator set on the record type
)
# If both the collection and the record type have an embedding generator set, the record type's embedding generator will be used for the collection. If neither is set, it is assumed the vector store itself can create embeddings, or that vectors are included in the records already, if that is not the case, it will likely raise.

# Enhanced collection operations
await collection.collection_exists()
await collection.ensure_collection_exists()
await collection.ensure_collection_deleted()

# CRUD methods
# Removed batch operations, all CRUD operations can now take both a single record or a list of records
records = [
    MyRecord(id="1", text="First record"),
    MyRecord(id="2", text="Second record")
]
ids = ["1", "2"]
# this method adds vectors automatically
await collection.upsert(records)

# You can do get with one or more ids, and it will return a list of records
await collection.get(ids)  # Returns a list of records
# you can also do a get without ids, with top, skip and order_by parameters
await collection.get(top=10, skip=0, order_by='id')
# the order_by parameter can be a string or a dict, with the key being the field name and the value being True for ascending or False for descending order.
# At this time, not all vector stores support this method.

# Delete also allows for single or multiple ids
await collection.delete(ids)

query = "search term"
# New search methods, these use the built-in embedding generator to take the value and create a vector
results = await collection.search(query, top=10)
results = await collection.hybrid_search(query, top=10)

# You can also supply a vector directly
query_vector = [0.1, 0.2, 0.3]  # Example vector
results = await collection.search(vector=query_vector, top=10)
results = await collection.hybrid_search(query, vector=query_vector, top=10)

Nowa implementacja magazynu wektorów przechodzi z obiektów FilterClause bazujących na ciągach znaków na bardziej zaawansowane i bezpieczne pod względem typów wyrażenia lambda lub filtry wywoływalne.

Przed: Obiekty FilterClause

from semantic_kernel.data.text_search import SearchFilter, EqualTo, AnyTagsEqualTo
from semantic_kernel.data.vector_search import VectorSearchFilter

# Creating filters using FilterClause objects
text_filter = SearchFilter()
text_filter.equal_to("category", "AI")
text_filter.equal_to("status", "active")

# Vector search filters
vector_filter = VectorSearchFilter()
vector_filter.equal_to("category", "AI")
vector_filter.any_tag_equal_to("tags", "important")

# Using in search
results = await collection.search(
    "query text",
    options=VectorSearchOptions(filter=vector_filter)
)

Po: Filtry wyrażeń lambda

# When defining the collection with the generic type hints, most IDE's will be able to infer the type of the record, so you can use the record type directly in the lambda expressions.
collection = InMemoryCollection[str, MyRecord](MyRecord)

# Using lambda expressions for more powerful and type-safe filtering
# The code snippets below work on a data model with more fields then defined earlier.

# Direct lambda expressions
results = await collection.search(
    "query text", 
    filter=lambda record: record.category == "AI" and record.status == "active"
)

# Complex filtering with multiple conditions
results = await collection.search(
    "query text",
    filter=lambda record: (
        record.category == "AI" and 
        record.score > 0.8 and
        "important" in record.tags
    )
)

# Combining conditions with boolean operators
results = await collection.search(
    "query text",
    filter=lambda record: (
        record.category == "AI" or record.category == "ML"
    ) and record.published_date >= datetime(2024, 1, 1)
)

# Range filtering (now possible with lambda expressions)
results = await collection.search(
    "query text",
    filter=lambda record: 0.5 <= record.confidence_score <= 0.9
)

Porady dotyczące migracji filtrów

  1. Prosta równość: filter.equal_to("field", "value") staje się lambda r: r.field == "value"
  2. Wiele warunków: Łączenie z operatorami and/or zamiast wielu wywołań filtru
  3. Zawieranie tagów/tablic: filter.any_tag_equal_to("tags", "value") staje się lambda r: "value" in r.tags
  4. Ulepszone możliwości: obsługa zapytań zakresu, złożonej logiki logicznej i niestandardowych predykatów

4. Ulepszona łatwość tworzenia funkcji wyszukiwania

Wcześniej: tworzenie funkcji wyszukiwania za pomocą elementu VectorStoreTextSearch

from semantic_kernel.connectors.in_memory import InMemoryCollection
from semantic_kernel.data import VectorStoreTextSearch

collection = InMemoryCollection(collection_name='collection', record_type=MyRecord)
search = VectorStoreTextSearch.from_vectorized_search(vectorized_search=collection, embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"))

search_function = search.create_search(
    function_name='search',
    ...
)

Po: Tworzenie funkcji wyszukiwania bezpośredniego

collection = InMemoryCollection(MyRecord)
# Create search function directly on collection
search_function = collection.create_search_function(
    function_name="search",
    search_type="vector",  # or "keyword_hybrid"
    top=10,
    vector_property_name="vector",  # Name of the vector field
)

# Add to kernel directly
kernel.add_function(plugin_name="memory", function=search_function)

5. Zmiana nazwy łącznika i importowanie zmian

Konsolidacja ścieżek importu

# Before: Scattered imports
from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore
from semantic_kernel.connectors.memory.chroma import ChromaMemoryStore
from semantic_kernel.connectors.memory.pinecone import PineconeMemoryStore
from semantic_kernel.connectors.memory.qdrant import QdrantMemoryStore

# After: Consolidated under connectors
from semantic_kernel.connectors.azure_ai_search import AzureAISearchStore
from semantic_kernel.connectors.chroma import ChromaVectorStore
from semantic_kernel.connectors.pinecone import PineconeVectorStore
from semantic_kernel.connectors.qdrant import QdrantVectorStore

# Alternative after: Consolidated with lazy loading:
from semantic_kernel.connectors.memory import (
    AzureAISearchStore,
    ChromaVectorStore,
    PineconeVectorStore,
    QdrantVectorStore,
    WeaviateVectorStore,
    RedisVectorStore
)

Zmiany nazw klas łącznika

Stara nazwa Nowa nazwa
AzureCosmosDBforMongoDB* CosmosMongo*
AzureCosmosDBForNoSQL* CosmosNoSql*

6. Ulepszenia wyszukiwania tekstu i usunięto łącznik Bing

Łącznik Bing został usunięty i ulepszony interfejs wyszukiwania tekstu

Łącznik wyszukiwania tekstu Bing został usunięty. Migrowanie do alternatywnych dostawców wyszukiwania:

# Before: Bing Connector (REMOVED)
from semantic_kernel.connectors.search.bing import BingConnector

bing_search = BingConnector(api_key="your-bing-key")

# After: Use Brave Search or other providers
from semantic_kernel.connectors.brave import BraveSearch
# or
from semantic_kernel.connectors.search import BraveSearch

brave_search = BraveSearch()

# Create text search function
text_search_function = brave_search.create_search_function(
    function_name="web_search",
    query_parameter_name="query",
    description="Search the web for information"
)

kernel.add_function(plugin_name="search", function=text_search_function)

Ulepszone metody wyszukiwania

Przed: trzy oddzielne metody wyszukiwania z różnymi typami zwracanymi

from semantic_kernel.connectors.brave import BraveSearch
brave_search = BraveSearch()
# Before: Separate search methods
search_results: KernelSearchResult[str] = await brave_search.search(
    query="semantic kernel python",
    top=5,
)

search_results: KernelSearchResult[TextSearchResult] = await brave_search.get_text_search_results(
    query="semantic kernel python",
    top=5,
)

search_results: KernelSearchResult[BraveWebPage] = await brave_search.get_search_results(
    query="semantic kernel python",
    top=5,
)

Po: Ujednolicona metoda wyszukiwania z parametrem rodzaju wyniku

from semantic_kernel.data.text_search import SearchOptions
# Enhanced search results with metadata
search_results: KernelSearchResult[str] = await brave_search.search(
    query="semantic kernel python",
    output_type=str, # can also be TextSearchResult or anything else for search engine specific results, default is `str`
    top=5,
    filter=lambda result: result.country == "NL",  # Example filter
)

async for result in search_results.results:
    assert isinstance(result, str)  # or TextSearchResult if using that type
    print(f"Result: {result}")
    print(f"Metadata: {search_results.metadata}")

7. Wycofanie starych magazynów pamięci

Wszystkie stare magazyny pamięci oparte na MemoryStoreBase zostały przeniesione do semantic_kernel.connectors.memory_stores i są teraz oznaczone jako zdeprecjonowane. Większość z nich ma równoważną nową implementację opartą na VectorStore i VectorStoreCollection, które można znaleźć w semantic_kernel.connectors.memory.

Te łączniki zostaną całkowicie usunięte:

  • AstraDB
  • Milvus
  • Usearch

Jeśli nadal potrzebujesz któregokolwiek z nich, upewnij się, że przejmiesz kod z przestarzałego modułu semantic_kernel.memory i folderu, lub zaimplementuj własną kolekcję magazynu wektorów na podstawie nowej VectorStoreCollection klasy.

Jeśli istnieje duże zapotrzebowanie na podstawie opinii github, rozważymy ich przywrócenie, ale na razie nie zostaną one zachowane i zostaną usunięte w przyszłości.

Migracja z semanticTextMemory

# Before: SemanticTextMemory (DEPRECATED)
from semantic_kernel.memory import SemanticTextMemory
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbeddingGenerationService

embedding_service = OpenAITextEmbeddingGenerationService(ai_model_id="text-embedding-3-small")
memory = SemanticTextMemory(storage=vector_store, embeddings_generator=embedding_service)

# Store memory
await memory.save_information(collection="docs", text="Important information", id="doc1")

# Search memory  
results = await memory.search(collection="docs", query="important", limit=5)
# After: Direct Vector Store Usage
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
from semantic_kernel.connectors.in_memory import InMemoryCollection

# Define data model
@vectorstoremodel
@dataclass
class MemoryRecord:
    id: Annotated[str, VectorStoreField('key')]
    text: Annotated[str, VectorStoreField('data', is_full_text_indexed=True)]
    embedding: Annotated[list[float] | str | None, VectorStoreField('vector', dimensions=1536, distance_function="cosine", embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"))] = None

# Create vector store with integrated embeddings
collection = InMemoryCollection(
    record_type=MemoryRecord,
    embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small")  # Optional, if not set on the record type
)

# Store with automatic embedding generation
record = MemoryRecord(id="doc1", text="Important information", embedding='Important information')
await collection.upsert(record)

# Search with built-in function
search_function = collection.create_search_function(
    function_name="search_docs",
    search_type="vector"
)

Migracja wtyczki pamięci

Jeśli chcesz mieć wtyczkę, która może również zapisywać informacje, można je łatwo utworzyć w następujący sposób:

# Before: TextMemoryPlugin (DEPRECATED)
from semantic_kernel.core_plugins import TextMemoryPlugin

memory_plugin = TextMemoryPlugin(memory)
kernel.add_plugin(memory_plugin, "memory")
# After: Custom plugin using vector store search functions
from semantic_kernel.functions import kernel_function

class VectorMemoryPlugin:
    def __init__(self, collection: VectorStoreCollection):
        self.collection = collection
    
    @kernel_function(name="save")
    async def save_memory(self, text: str, key: str) -> str:
        record = MemoryRecord(id=key, text=text, embedding=text)
        await self.collection.upsert(record)
        return f"Saved to {self.collection.collection_name}"
    
    @kernel_function(name="search") 
    async def search_memory(self, query: str, limit: int = 5) -> str:
        results = await self.collection.search(
            query, top=limit, vector_property_name="embedding"
        )        
        return "\n".join([r.record.text async for r in results.results])

# Register the new plugin
memory_plugin = VectorMemoryPlugin(collection)
kernel.add_plugin(memory_plugin, "memory")

Krok 1. Aktualizowanie importu

  • [ ] Zastąp importy pamięci odpowiednikami magazynów wektorów
  • [ ] Zaktualizuj importy pól do użycia VectorStoreField
  • [ ] Usuwanie importu łącznika Bing

Krok 2. Aktualizowanie definicji pól

  • [ ] Konwertuj na ujednoliconą klasę VectorStoreField
  • [ ] Aktualizowanie nazw właściwości (is_filterableis_indexed)
  • [ ] Dodawanie zintegrowanych generatorów osadzania do pól wektorowych

Krok 3. Aktualizowanie użycia kolekcji

  • [ ] Zastępowanie operacji pamięci metodami magazynu wektorowego
  • [ ] Użyj nowych operacji wsadowych, jeśli ma to zastosowanie
  • [ ] Implementowanie tworzenia nowej funkcji wyszukiwania

Krok 4. Aktualizacja implementacji wyszukiwania

  • [ ] Zastępowanie funkcji wyszukiwania ręcznego za pomocą polecenia create_search_function
  • [ ] Aktualizowanie wyszukiwania tekstu w celu używania nowych dostawców
  • [ ] Implementowanie wyszukiwania hybrydowego tam, gdzie korzystne
  • [ ] Migrowanie z FilterClause do lambda wyrażeń na potrzeby filtrowania

Krok 5. Usuwanie przestarzałego kodu

  • [ ] Usuń SemanticTextMemory użycie
  • [ ] Usuwanie TextMemoryPlugin zależności

Korzyści z wydajności i funkcji

Ulepszenia wydajności

  • Operacje wsadowe: nowe metody operacji upsert/delete wsadowych zwiększają przepływność
  • Zintegrowane osadzanie: eliminuje oddzielne kroki generowania osadzania
  • Zoptymalizowane wyszukiwanie: wbudowane funkcje wyszukiwania są zoptymalizowane pod kątem każdego typu magazynu

Ulepszenia funkcji

  • Wyszukiwanie hybrydowe: łączy wyszukiwanie wektorów i tekstu w celu uzyskania lepszych wyników
  • Zaawansowane filtrowanie: ulepszone wyrażenia filtrów i indeksowanie

Środowisko dewelopera

  • Uproszczony interfejs API: mniej klas i metod do nauki
  • Spójny interfejs: ujednolicone podejście do wszystkich magazynów wektorów
  • Lepsza dokumentacja: Przejrzyste przykłady i ścieżki migracji
  • Weryfikacja przyszłości: dopasowywana do zestawu .NET SDK w celu zapewnienia spójnego programowania międzyplatformowego

Podsumowanie

Omówione powyżej aktualizacje magazynu wektorów reprezentują znaczącą poprawę zestawu SDK języka Python jądra semantycznego. Nowa ujednolicona architektura zapewnia lepszą wydajność, ulepszone funkcje i bardziej intuicyjne środowisko deweloperskie. Chociaż migracja wymaga aktualizacji importów i refaktoryzacji istniejącego kodu, korzyści z konserwacji i funkcjonalności sprawiają, że to uaktualnienie jest zdecydowanie zalecane.

Aby uzyskać dodatkową pomoc dotyczącą migracji, zapoznaj się ze zaktualizowanymi przykładami w samples/concepts/memory/ katalogu i kompleksową dokumentacją interfejsu API.