Files
microsoft--semantic-kernel/python/semantic_kernel/connectors/faiss.py
T
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

279 lines
12 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import MutableMapping, Sequence
from typing import Any, Final, Generic
import faiss
import numpy as np
from pydantic import Field
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.in_memory import IN_MEMORY_SCORE_KEY, InMemoryCollection, InMemoryStore, TKey
from semantic_kernel.data.vector import (
DistanceFunction,
IndexKind,
KernelSearchResults,
SearchType,
TModel,
VectorSearchOptions,
VectorSearchResult,
VectorStoreCollectionDefinition,
VectorStoreField,
)
from semantic_kernel.exceptions import VectorStoreInitializationException, VectorStoreOperationException
from semantic_kernel.exceptions.vector_store_exceptions import VectorStoreModelException
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
logger = logging.getLogger(__name__)
DISTANCE_FUNCTION_MAP: Final[dict[DistanceFunction, type[faiss.Index]]] = {
DistanceFunction.EUCLIDEAN_SQUARED_DISTANCE: faiss.IndexFlatL2,
DistanceFunction.DOT_PROD: faiss.IndexFlatIP,
DistanceFunction.DEFAULT: faiss.IndexFlatL2,
}
INDEX_KIND_MAP: Final[dict[IndexKind, bool]] = {
IndexKind.FLAT: True,
IndexKind.DEFAULT: True,
}
def _create_index(field: VectorStoreField) -> faiss.Index:
"""Create a Faiss index."""
if field.index_kind not in INDEX_KIND_MAP:
raise VectorStoreInitializationException(f"Index kind {field.index_kind} is not supported.")
if field.distance_function not in DISTANCE_FUNCTION_MAP:
raise VectorStoreInitializationException(f"Distance function {field.distance_function} is not supported.")
match field.index_kind:
case IndexKind.FLAT | IndexKind.DEFAULT:
match field.distance_function:
case DistanceFunction.EUCLIDEAN_SQUARED_DISTANCE | DistanceFunction.DEFAULT:
return faiss.IndexFlatL2(field.dimensions)
case DistanceFunction.DOT_PROD:
return faiss.IndexFlatIP(field.dimensions)
case _:
raise VectorStoreInitializationException(
f"Distance function {field.distance_function} is "
f"not supported for index kind {field.index_kind}."
)
case _:
raise VectorStoreInitializationException(f"Index with {field.index_kind} is not supported.")
class FaissCollection(InMemoryCollection[TKey, TModel], Generic[TKey, TModel]):
"""Create a Faiss collection.
The Faiss Collection builds on the InMemoryVectorCollection,
it maintains indexes and mappings for each vector field.
"""
indexes: MutableMapping[str, faiss.Index] = Field(default_factory=dict)
indexes_key_map: MutableMapping[str, MutableMapping[TKey, int]] = Field(default_factory=dict)
def __init__(
self,
record_type: type[TModel],
definition: VectorStoreCollectionDefinition | None = None,
collection_name: str | None = None,
embedding_generator: EmbeddingGeneratorBase | None = None,
**kwargs: Any,
):
"""Create a Faiss Collection.
To allow more complex index setups, you can pass them in here:
```python
import faiss
index = faiss.IndexFlatL2(128)
FaissCollection(..., indexes={"vector_field_name": index})
```
or you can manually add them to the indexes field of the collection.
Args:
collection_name: The name of the collection.
record_type: The type of the data model.
definition: The definition of the data model.
embedding_generator: The embedding generator.
kwargs: Additional arguments.
"""
super().__init__(
record_type=record_type,
definition=definition,
collection_name=collection_name,
embedding_generator=embedding_generator,
**kwargs,
)
def _create_indexes(self, index: faiss.Index | None = None, indexes: dict[str, faiss.Index] | None = None) -> None:
"""Create Faiss indexes for each vector field.
Args:
index: The index to use, this can be used when there is only one vector field.
indexes: A dictionary of indexes, the key is the name of the vector field.
"""
if len(self.definition.vector_fields) == 1 and index is not None:
if not isinstance(index, faiss.Index):
raise VectorStoreInitializationException("Index must be a subtype of faiss.Index")
if not index.is_trained:
raise VectorStoreInitializationException("Index must be trained before using.")
self.indexes[self.definition.vector_fields[0].name] = index
return
for vector_field in self.definition.vector_fields:
if indexes and vector_field.name in indexes:
if not isinstance(indexes[vector_field.name], faiss.Index):
raise VectorStoreInitializationException(
f"Index for {vector_field.name} must be a subtype of faiss.Index"
)
if not indexes[vector_field.name].is_trained:
raise VectorStoreInitializationException(
f"Index for {vector_field.name} must be trained before using."
)
self.indexes[vector_field.name] = indexes[vector_field.name]
if vector_field.name not in self.indexes_key_map:
self.indexes_key_map.setdefault(vector_field.name, {})
continue
if vector_field.name not in self.indexes:
self.indexes[vector_field.name] = _create_index(vector_field)
if vector_field.name not in self.indexes_key_map:
self.indexes_key_map.setdefault(vector_field.name, {})
@override
async def ensure_collection_exists(
self, index: faiss.Index | None = None, indexes: dict[str, faiss.Index] | None = None, **kwargs: Any
) -> None:
"""Create a collection.
Considering the complexity of different faiss indexes, we support a limited set.
For more advanced scenario's you can create your own indexes and pass them in here.
This includes indexes that need training, or GPU-based indexes, since you would also
need to build the faiss package for use with GPU's yourself.
Args:
index: The index to use, this can be used when there is only one vector field.
indexes: A dictionary of indexes, the key is the name of the vector field.
kwargs: Additional arguments.
"""
self._create_indexes(index=index, indexes=indexes)
@override
async def _inner_upsert(self, records: Sequence[Any], **kwargs: Any) -> Sequence[TKey]:
"""Upsert records."""
for vector_field in self.definition.vector_fields:
vectors_to_add = [record.get(vector_field.storage_name or vector_field.name) for record in records]
vectors = np.array(vectors_to_add, dtype=np.float32)
if not self.indexes[vector_field.name].is_trained:
raise VectorStoreOperationException(
f"This index (of type {type(self.indexes[vector_field.name])}) requires training, "
"which is not supported. To train the index, "
f"use <collection>.indexes[{vector_field.name}].train, "
"see faiss docs for more details."
)
self.indexes[vector_field.name].add(vectors) # type: ignore
start = len(self.indexes_key_map[vector_field.name])
for i, record in enumerate(records):
key = record[self.definition.key_field.name]
self.indexes_key_map[vector_field.name][key] = start + i
return await super()._inner_upsert(records, **kwargs)
@override
async def _inner_delete(self, keys: Sequence[TKey], **kwargs: Any) -> None:
for key in keys:
for vector_field in self.definition.vector_field_names:
if key in self.indexes_key_map[vector_field]:
vector_index = self.indexes_key_map[vector_field][key]
self.indexes[vector_field].remove_ids(np.array([vector_index]))
self.indexes_key_map[vector_field].pop(key, None)
await super()._inner_delete(keys, **kwargs)
@override
async def ensure_collection_deleted(self, **kwargs: Any) -> None:
for vector_field in self.definition.vector_field_names:
if vector_field in self.indexes:
del self.indexes[vector_field]
if vector_field in self.indexes_key_map:
del self.indexes_key_map[vector_field]
await super().ensure_collection_deleted(**kwargs)
@override
async def collection_exists(self, **kwargs: Any) -> bool:
return bool(self.indexes)
@override
async def _inner_search(
self,
search_type: SearchType,
options: VectorSearchOptions,
values: Any | None = None,
vector: Sequence[float | int] | None = None,
**kwargs: Any,
) -> KernelSearchResults[VectorSearchResult[TModel]]:
"""Inner search method."""
if not vector:
vector = await self._generate_vector_from_values(values, options)
field = self.definition.try_get_vector_field(options.vector_property_name)
if not field:
raise VectorStoreModelException(
f"Vector field '{options.vector_property_name}' not found in the data model definition."
)
return_list = []
# first we create the vector to search with
np_vector = np.array(vector, dtype=np.float32).reshape(1, -1)
# then do the actual vector search
distances, indexes = self.indexes[field.name].search(
np_vector, min(options.top, self.indexes[field.name].ntotal)
) # type: ignore[call-arg]
# since Faiss indexes do not contain the full records,
# we get the filtered records, this is a dict of the records that match the search filters
# and use that to get the actual records
filtered_records = self._get_filtered_records(options)
# we then iterate through the results, the order is the order of relevance
# (less or most distance, dependant on distance metric used)
for i, index in enumerate(indexes[0]):
key = list(self.indexes_key_map[field.name].keys())[index]
# if the key is not in the filtered records, we ignore it
if key not in filtered_records:
continue
filtered_records[key][IN_MEMORY_SCORE_KEY] = distances[0][i]
# so we return the list in the order of the search, with the record from the inner_storage.
return_list.append(filtered_records[key])
return KernelSearchResults(
results=self._get_vector_search_results_from_results(return_list, options),
total_count=len(return_list) if options and options.include_total_count else None,
)
class FaissStore(InMemoryStore):
"""Create a Faiss store."""
def __init__(
self,
embedding_generator: EmbeddingGeneratorBase | None = None,
**kwargs: Any,
):
"""Create a Faiss store."""
super().__init__(embedding_generator=embedding_generator, **kwargs)
@override
def get_collection(
self,
record_type: type[TModel],
*,
definition: VectorStoreCollectionDefinition | None = None,
collection_name: str | None = None,
embedding_generator: EmbeddingGeneratorBase | None = None,
**kwargs: Any,
) -> FaissCollection:
"""Get a Faiss collection."""
return FaissCollection(
collection_name=collection_name,
record_type=record_type,
definition=definition,
embedding_generator=embedding_generator or self.embedding_generator,
**kwargs,
)