Guia de Migração do Kernel Semântico para o Repositório de Vetores Python

Visão geral

Este guia aborda as principais atualizações do repositório de vetores introduzidas no Kernel Semântico versão 1.34, que representa uma revisão significativa da implementação do repositório de vetores para se alinhar ao SDK do .NET e fornecer uma API mais unificada e intuitiva. As alterações consolidam tudo sob semantic_kernel.data.vector e melhoram a arquitetura do conector.

Resumo das principais melhorias

  • Modelo de campo unificado: classe única VectorStoreField substitui vários tipos de campo
  • Inserções Integradas: Geração de inserção direta em especificações de campo de vetor
  • Pesquisa Simplificada: criação fácil de funções de pesquisa diretamente em coleções
  • Estrutura Consolidada: Tudo em semantic_kernel.data.vector e semantic_kernel.connectors
  • Pesquisa de Texto Aprimorada: Recursos aprimorados de pesquisa de texto com conectores simplificados
  • Preterição: os antigos memory_stores foram descontinuados em favor da nova arquitetura do repositório de vetores

1. Inserções integradas e atualizações de campos/modelos de repositório de vetores

Há várias alterações na maneira como você define seu modelo de repositório de vetores, a maior é que agora oferecemos suporte a inserções integradas diretamente nas definições de campo do repositório de vetores. Isso significa que, quando você especifica um campo para ser um vetor, o conteúdo desse campo é inserido automaticamente usando o gerador de inserção especificado, como o modelo de inserção de texto do OpenAI. Isso simplifica o processo de criação e gerenciamento de campos de vetor.

Ao definir esse campo, você precisa ter certeza de três coisas, especialmente ao usar um modelo Pydantic:

  1. tipagem: o campo provavelmente terá três tipos: list[float], str ou algo mais para a entrada no gerador de inserção, e None para quando o campo estiver não definido.
  2. valor padrão: o campo deve ter um valor padrão de None ou algum outro valor, para que não haja erro ao obter registros de get ou search com include_vectors=False, que é o padrão agora.

Há duas preocupações aqui, a primeira é que, ao decorar uma classe com vectorstoremodel, a anotação do primeiro tipo do campo é usada para preencher o type parâmetro da VectorStoreField classe, portanto, você precisa ter certeza de que a anotação de primeiro tipo é o tipo certo para a coleção de repositório de vetores a ser criada com, muitas vezes list[float]. Por padrão, os métodos get e search não incluem os vetores nos resultados; portanto, o campo precisa ter um valor padrão, e a tipagem precisa corresponder a isso. Assim, muitas vezes None é permitido, e o padrão é definido como None. Quando o campo é criado, os valores que precisam ser inseridos estão nesse campo, muitas vezes cadeias de caracteres, portanto str , também precisam ser incluídos. O motivo dessa alteração é permitir mais flexibilidade no que está inserido e o que é realmente armazenado em campos de dados, essa seria uma configuração comum:

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}"

Observe que o método post_init , isso cria um valor que é inserido, que é mais do que um único campo. Os três tipos também estão presentes.

Antes: separe as classes de campo

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")
]

Depois: VectorStoreField unificado com Embeddings integrados

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
    )
]

Alterações de chave na definição de campo

  1. Classe de campo único: VectorStoreField substitui todos os tipos de campo anteriores
  2. Especificação do tipo de campo: use o parâmetro field_type: Literal["key", "data", "vector"], que pode ser um parâmetro posicional, portanto VectorStoreField("key") é válido.
  3. Propriedades aprimoradas:
    • storage_name foi adicionado, quando definido, que é usado como o nome do campo no repositório de vetores, caso contrário, o name parâmetro é usado.
    • dimensions agora é um parâmetro necessário para campos de vetor.
    • distance_function e index_kind são opcionais e serão definidos como DistanceFunction.DEFAULT e IndexKind.DEFAULT, respectivamente, se não forem especificados e somente para campos de vetor, cada implementação de repositório de vetores possui uma lógica que seleciona um padrão para esse repositório.
  4. Renomeações de propriedades:
    • property_typetype_ como um atributo e type em construtores
    • is_filterableis_indexed
    • is_full_text_searchableis_full_text_indexed
  5. Inserções Integradas: Adicione embedding_generator diretamente aos campos de vetor, como alternativa, você pode definir a embedding_generator coleção do repositório de vetor em si, que será usada para todos os campos de vetor nesse repositório, esse valor tem precedência sobre o gerador de inserção no nível da coleção.

2. Novos métodos em lojas e coleções

Interface de repositório aprimorada

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.

Interface de coleção aprimorada

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)

A nova implementação do armazenamento de vetor passa de objetos FilterClause baseados em cadeia de caracteres para expressões lambda ou filtros chamáveis mais avançados e seguros quanto ao tipo.

Antes: objetos 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)
)

Depois: Filtros de expressão 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
)

Dicas de migração para filtros

  1. Igualdade simples: filter.equal_to("field", "value") torna-se lambda r: r.field == "value"
  2. Várias condições: encadear com operadores and/or ao invés de múltiplas chamadas de filtro
  3. Contenção de marca/matriz: filter.any_tag_equal_to("tags", "value") torna-se lambda r: "value" in r.tags
  4. Funcionalidades aprimoradas: suporte para consultas de intervalo, lógica booliana complexa e predicados personalizados

4. Facilidade aprimorada de criação de funções de pesquisa

Antes: Criar função de pesquisa com 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',
    ...
)

Depois: Criação de função de pesquisa direta

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. O conector renomeia e importa alterações

Consolidação do caminho de importação

# 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
)

Renomeação de classes de conectores

Nome antigo Novo nome
AzureCosmosDBforMongoDB* CosmosMongo*
AzureCosmosDBForNoSQL* CosmosNoSql*

6. Aprimoramentos de pesquisa de texto e conector do Bing removido

Conector do Bing removido e interface de pesquisa de texto aprimorada

O conector de pesquisa de texto do Bing foi removido. Migrar para provedores de pesquisa alternativos:

# 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)

Métodos de pesquisa aprimorados

Antes: três métodos de pesquisa separados com tipos de retorno diferentes

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,
)

Depois: método de busca unificado com parâmetro de tipo de saída

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. Depreciação de repositórios de memória antigos

Todos os repositórios de memória antigos, baseados em MemoryStoreBase, foram movidos para semantic_kernel.connectors.memory_stores e agora estão marcados como preteridos. A maioria deles tem uma nova implementação equivalente baseada em VectorStore e VectorStoreCollection, que pode ser encontrada em semantic_kernel.connectors.memory.

Esses conectores serão completamente removidos:

  • AstraDB
  • Milvus
  • Usearch

Se você precisar de algum deles ainda, certifique-se de assumir o código do módulo preterido e da semantic_kernel.memory pasta ou implementar sua própria coleção de repositórios de vetores com base na nova VectorStoreCollection classe.

Se houver uma grande demanda com base nos comentários do github, consideraremos trazê-los de volta, mas, por enquanto, eles não serão mantidos e serão removidos no futuro.

Migração de 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"
)

Migração de plug-ins de memória

Quando você quiser ter um plug-in que também possa salvar informações, você pode criar facilmente assim:

# 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")

Etapa 1: Atualizar importações

  • [ ] Substituir importações do repositório de memória por equivalentes do repositório de vetores
  • [ ] Atualizar as importações de campo para usar VectorStoreField
  • [ ] Remover importações de conector do Bing

Etapa 2: Atualizar definições de campo

  • [ ] Converter em classe unificada VectorStoreField
  • [ ] Atualizar nomes de propriedade (is_filterableis_indexed)
  • [ ] Adicionar geradores de inserção integrados a campos de vetor

Etapa 3: Atualizar o uso da coleção

  • [ ] Substituir operações de memória por métodos de repositório de vetores
  • [ ] Usar novas operações em lote quando aplicável
  • [ ] Implementar a criação da nova função de pesquisa

Etapa 4: Atualizar implementação de pesquisa

  • [ ] Substituir funções de pesquisa manuais por create_search_function
  • [ ] Atualizar a pesquisa de texto para usar novos provedores
  • [ ] Implementar a pesquisa híbrida onde for benéfico
  • [ ] Migrar de FilterClause para lambda expressões para filtragem

Etapa 5: Remover código preterido

  • [ ] Remover o uso de SemanticTextMemory
  • [ ] Remover dependências TextMemoryPlugin

Benefícios de Desempenho e de Características

Melhorias de desempenho

  • Operações em lote: Novos métodos de upsert/delete em lote melhoram o desempenho
  • Inserções Integradas: elimina etapas separadas de geração de inserção
  • Pesquisa otimizada: as funções de pesquisa internas são otimizadas para cada tipo de repositório

Aprimoramentos de funcionalidades

  • Pesquisa Híbrida: combina pesquisa de vetor e texto para obter melhores resultados
  • Filtragem avançada: expressões de filtro avançadas e indexação

Experiência do desenvolvedor

  • API simplificada: menos classes e métodos para aprender
  • Interface Consistente: abordagem unificada em todos os repositórios de vetores
  • Melhor documentação: exemplos claros e caminhos de migração claros
  • Preparado para o futuro: alinhado com o SDK do .NET para desenvolvimento consistente entre plataformas

Conclusão

As atualizações do repositório de vetores discutidas acima representam uma melhoria significativa no SDK do Python do Kernel Semântico. A nova arquitetura unificada fornece melhor desempenho, recursos aprimorados e uma experiência de desenvolvedor mais intuitiva. Embora a migração exija a atualização das importações e a refatoração do código existente, os benefícios na manutenção e na funcionalidade tornam essa atualização altamente recomendada.

Para obter ajuda adicional com a migração, consulte os exemplos atualizados no samples/concepts/memory/ diretório e a documentação abrangente da API.