Почему Weaviate

У Qdrant и Chroma вы сами реализуете hybrid search: берёте BM25 из Elasticsearch или BM25s-библиотеки, считаете sparse-вектора, запускаете два запроса, сливаете результаты вручную. В Weaviate это встроено из коробки: один вызов query.hybrid() — и база сама делает BM25 по инвертированному индексу, векторный поиск по HNSW, нормирует оба Score и сливает через RelativeScoreFusion.

Второй ключевой кейс — multi-tenancy: если строите SaaS-продукт, где у каждого клиента свои данные, Weaviate нативно изолирует тенантов внутри одной коллекции без дополнительной инфраструктуры. Qdrant тоже поддерживает, но в Weaviate это глубже интегрировано на уровне схемы.

Задача
Chroma
Qdrant
Weaviate
Hybrid search (BM25 + vector)
Нет
Sparse vectors + RRF
Встроен, один вызов
Vectorizer из коробки
Нет
Нет
OpenAI, Cohere, Ollama…
Generative search (RAG)
Нет
Нет
Встроен в запрос
Multi-tenancy
Нет
Есть
Нативная, миллионы тенантов
Несколько векторов на объект
Нет
Named vectors
Named vectors + multi-modal
Порог потребления памяти
Малый (SQLite)
Минимальный (Rust)
Средний (Go + HNSW в RAM)
Лучший сценарий
Прототип, <1M
Максимум перф., self-hosted
Hybrid RAG, SaaS, знания-граф

Архитектура: три индекса вместо одного

Большинство векторных баз хранят только один вид индекса — HNSW для приближённого поиска ближайших соседей. Weaviate хранит три независимых структуры для каждой коллекции:

  • HNSW vector index — иерархический граф для семантического поиска; держится в RAM, восстанавливается из WAL после рестарта.
  • BM25 inverted index — инвертированный индекс слов для ключевого поиска; хранится на диске, LSM-дерево сегментов.
  • LSM-tree object storage — сами объекты + их свойства; memtable в RAM, сегменты на диске, фоновое слияние.
100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Python v4 Client gRPC (50050) · REST (8080) Collection «Articles» Schema · Vectorizer config HNSW Vector Index Graph в RAM Восстановление: WAL near_text / near_vector BM25 Inverted Index Токенизация слов LSM сегменты на диске query.bm25() LSM-tree Objects Memtable → Segments Фоновое слияние Properties · Payload WAL Write-Ahead Log · Crash recovery HYBRID SEARCH FLOW query = «machine learning» alpha=0.75 · limit=10 Vectorizer text → embedding HNSW kNN search Tokenizer «machine», «learning» BM25 scoring vector score [0,1] 0.92 · 0.87 · 0.81… BM25 score (raw) 14.2 · 9.7 · 8.1… RelativeScoreFusion Нормализация: max→1.0, min→0.0 score = 0.75×vec + 0.25×bm25 WHERE Filter Pre/post filter indexFilterable Ranked Results Autocut · Rerank · Generative #1 score=0.94 #2 score=0.88 #3 score=0.81

WAL (Write-Ahead Log) — ключ к надёжности. При записи Weaviate сначала пишет в WAL, потом в memtable. При падении — восстанавливает HNSW граф и объекты по логу. Для HNSW WAL содержит результаты вычислений (куда поместить вектор, каких соседей связать), а не сырые данные.

Память vs диск. HNSW граф живёт полностью в RAM — это значит, что при 1 миллионе векторов размером 1536 float32 нужно ~6 ГБ только под граф (без объектов). Для больших коллекций планируйте память заранее.

Schema: коллекции, свойства, векторайзеры

В Weaviate данные хранятся в коллекциях (раньше назывались «классами»). Коллекция — это как таблица в PostgreSQL, только с векторным индексом. Схема определяет: какие свойства есть у объектов, как их токенизировать для BM25, какой векторайзер использовать.

import weaviate
from weaviate.classes.config import Configure, Property, DataType, Tokenization

with weaviate.connect_to_local() as client:

    client.collections.create(
        name="Articles",

        # Свойства объектов
        properties=[
            Property(
                name="title",
                data_type=DataType.TEXT,
                tokenization=Tokenization.WORD,          # BM25: разбивать по словам
                index_searchable=True,                   # Включить в BM25 поиск
                index_filterable=True,                   # Включить в фильтры
            ),
            Property(
                name="content",
                data_type=DataType.TEXT,
                tokenization=Tokenization.WORD,
                vectorize_property_name=False,           # Не добавлять "content:" к векторизации
            ),
            Property(
                name="category",
                data_type=DataType.TEXT,
                tokenization=Tokenization.FIELD,         # Целиком как один токен (для точного match)
                skip_vectorization=True,                 # Не влияет на embedding
            ),
            Property(
                name="published_at",
                data_type=DataType.DATE,
                index_range_filters=True,                # Оптимизация для >, <, >=, <=
            ),
            Property(
                name="rating",
                data_type=DataType.NUMBER,
                index_range_filters=True,
            ),
            Property(
                name="tags",
                data_type=DataType.TEXT_ARRAY,           # Массив строк
                tokenization=Tokenization.WORD,
            ),
            Property(
                name="source_id",
                data_type=DataType.TEXT,
                skip_vectorization=True,
                index_searchable=False,                  # Только фильтры, не BM25
            ),
        ],

        # Векторайзер: автоматически векторизует объекты при вставке
        vector_config=Configure.Vectors.text2vec_openai(
            model="text-embedding-3-small",
        ),

        # Generative модель для RAG-запросов
        generative_config=Configure.Generative.openai(
            model="gpt-4o-mini",
        ),
    )

Типы данных

DataType
Python тип
Описание
TEXT / TEXT_ARRAY
str / list[str]
Строки; поддерживают BM25 и фильтры
INT / INT_ARRAY
int / list[int]
Целые числа; поддерживают range-фильтры
NUMBER / NUMBER_ARRAY
float / list[float]
Числа с плавающей точкой
BOOLEAN
bool
true/false; только фильтры
DATE / DATE_ARRAY
datetime / str RFC3339
Дата и время в формате RFC 3339
UUID / UUID_ARRAY
str (UUID формат)
Идентификаторы; exact match только
GEO_COORDINATES
dict {lat, lon}
Геолокация; поддержка geoWithin фильтров
OBJECT / OBJECT_ARRAY
dict / list[dict]
Вложенные объекты; nested свойства

Подключение и базовые операции

Python v4 клиент вышел как GA в 2024. Он использует gRPC вместо REST для большинства операций — это ускоряет bulk import и запросы в ~3–5 раз. Порт gRPC по умолчанию: 50050.

import weaviate
from weaviate.auth import AuthApiKey

# Локальный Docker (gRPC: 50050, REST: 8080)
client = weaviate.connect_to_local()

# Weaviate Cloud (managed)
client = weaviate.connect_to_weaviate_cloud(
    cluster_url="https://your-cluster.weaviate.cloud",
    auth_credentials=AuthApiKey("your-wcd-api-key"),
)

# Embedded (в процессе Python, для тестов)
client = weaviate.connect_to_embedded(
    version="1.26.0",
    persistence_data_path="./weaviate_data",
)

# ВСЕГДА используй контекстный менеджер — он закроет gRPC-соединение
with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")
    # ... ваши операции ...

CRUD операции

from weaviate.util import generate_uuid4
import weaviate.classes as wvc

with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")

    # ── Insert single ──────────────────────────────────────────────────
    uuid = articles.data.insert(
        properties={
            "title": "Введение в RAG",
            "content": "RAG (Retrieval-Augmented Generation) — подход...",
            "category": "AI",
            "rating": 4.8,
            "tags": ["rag", "llm", "python"],
        },
        uuid=generate_uuid4(),   # опционально; генерируется автоматически
    )
    print(uuid)  # UUID вставленного объекта

    # ── Bulk insert (в 10-100× быстрее одиночных) ─────────────────────
    objects = [
        wvc.data.DataObject(
            properties={
                "title": f"Статья {i}",
                "content": "...",
                "category": "Tech",
                "rating": 4.0,
            },
            uuid=generate_uuid4(),
        )
        for i in range(1000)
    ]
    result = articles.data.insert_many(objects)
    if result.has_errors:
        for err in result.errors.values():
            print(err)

    # ── Update (partial — только указанные поля) ──────────────────────
    articles.data.update(
        uuid=uuid,
        properties={"rating": 4.9},   # остальные поля не трогаются
    )

    # ── Replace (full — остаются только указанные поля) ───────────────
    articles.data.replace(
        uuid=uuid,
        properties={"title": "Новый заголовок", "content": "Новый контент"},
    )

    # ── Delete by ID ──────────────────────────────────────────────────
    articles.data.delete_by_id(uuid)

    # ── Delete by filter ──────────────────────────────────────────────
    from weaviate.classes.query import Filter
    articles.data.delete_many(
        where=Filter.by_property("category").equal("Draft")
    )

    # ── Get single object ─────────────────────────────────────────────
    obj = articles.query.fetch_object_by_id(
        uuid=uuid,
        return_properties=["title", "rating"],
    )
    print(obj.properties)

Hybrid search в Weaviate — это не просто «запустить два запроса». Это единый механизм с общим пайплайном нормализации и слияния. Ключевой параметр — alpha, который балансирует между семантическим и ключевым поиском.

Параметр alpha
Вес векторного поиска: score = alpha × vec_score + (1−alpha) × bm25_score
0.0
0.5
0.75
1.0
← 100% BM25 Баланс 100% Vector →
alpha = 0.0
Только BM25. Когда важна точность ключевых слов: юридические термины, коды, артикулы.
alpha = 0.75
Рекомендованный старт для RAG. Семантика доминирует, BM25 страхует точные совпадения.
alpha = 1.0
Только векторный поиск. Когда запрос концептуальный и ключевые слова не важны.

Типы Fusion

Прежде чем складывать оценки через alpha, нужно нормализовать их — у BM25 и векторного поиска разные диапазоны. Weaviate предлагает два алгоритма:

Fusion Type
По умолчанию
Как работает
RELATIVE_SCORE_FUSION
с v1.24
Нормализует оба score: max→1.0, min→0.0. Сохраняет реальные пропорции расстояний. Работает с auto_limit. Рекомендуется.
RANKED_FUSION
до v1.24
RRF: использует только позиции в ранке (1/rank). Не работает с auto_limit. Менее точен, но стабилен.
from weaviate.classes.query import HybridFusion

with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")

    results = articles.query.hybrid(
        query="управление выдачей в RAG",
        alpha=0.75,                              # 75% vector, 25% BM25
        fusion_type=HybridFusion.RELATIVE_SCORE, # нормализация по score
        limit=10,
        return_metadata=["score", "explain_score"],  # показать итоговый score
    )

    for obj in results.objects:
        print(obj.properties["title"])
        print(f"  score: {obj.metadata.score:.4f}")

BM25 токенизация

Токенизация определяет, как Weaviate разбивает текст при индексировании и при поиске. Выбор напрямую влияет на качество keyword search.

Режим
Входная строка
Токены
Когда использовать
WORD
«Hello-World»
["hello", "world"]
Статьи, описания. По умолчанию.
LOWERCASE
«Hello-World»
["hello-world"]
Email, URL, код — сохраняет символы.
WHITESPACE
«Hello-World»
["Hello-World"]
Акронимы, имена собственные (case-sensitive).
FIELD
«Hello-World»
["Hello-World"]
UUID, категории — точное совпадение всей строки.

Управление выдачей: все рычаги

Это самый важный раздел. Weaviate даёт несколько независимых механизмов влиять на то, что вернётся и в каком порядке: режим запроса, пороги distance/certainty, фильтры, autocut, named vectors, reranking и generative search.

Режимы запроса

Hybrid Search
collection.query.hybrid(query, alpha, ...)
BM25 + векторный поиск с настраиваемым балансом. Лучший выбор для большинства RAG-сценариев.
Когда: поисковый запрос на естественном языке + важны точные слова.
Near Text / Vector / Object
collection.query.near_text(query, distance, ...)
Чистый векторный поиск. near_text автовекторизует запрос, near_vector — принимает готовый вектор.
Когда: концептуальный поиск, поиск похожих документов.
BM25 Keyword
collection.query.bm25(query, ...)
Только инвертированный индекс. Быстро, точно по ключевым словам, без векторизации запроса.
Когда: поиск по коду, артикулам, юридическим терминам.
Filter-only
collection.query.fetch_objects(where, sort, ...)
Только структурированная выборка без семантики. Работает как обычный SQL SELECT с WHERE.
Когда: пагинация каталога, отчёты, экспорт данных.

Distance и Certainty: пороги релевантности

Оба параметра ограничивают выдачу по близости к запросу. Это главный способ отсечь нерелевантные результаты в векторном поиске.

from weaviate.classes.query import MetadataQuery

with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")

    # ── distance: максимальное расстояние (0 = идентично, 2 = противоположно) ──
    # Для cosine distance: 0 = идентично, 1 = ортогонально, 2 = противоположно
    results = articles.query.near_text(
        query="machine learning basics",
        distance=0.4,          # отсекаем всё с distance > 0.4
        limit=20,
        return_metadata=MetadataQuery(distance=True),
    )

    # ── certainty: уверенность 0.0–1.0 (только для cosine) ─────────────
    # certainty = 1 - distance/2 (преобразование из distance)
    results = articles.query.near_text(
        query="machine learning basics",
        certainty=0.8,         # хотим уверенность >= 80%
        limit=20,
        return_metadata=MetadataQuery(certainty=True, distance=True),
    )

    for obj in results.objects:
        print(f"{obj.properties['title']}")
        print(f"  distance={obj.metadata.distance:.3f}")
        print(f"  certainty={obj.metadata.certainty:.3f}")

    # ── В hybrid: score_threshold вместо distance ────────────────────
    # score в hybrid — это нормализованный 0–1, выше = лучше
    results = articles.query.hybrid(
        query="...",
        alpha=0.75,
        limit=20,
        return_metadata=MetadataQuery(score=True),
    )
    # Фильтруем вручную (нет прямого score_threshold в hybrid v4)
    relevant = [o for o in results.objects if o.metadata.score > 0.6]

Фильтры: WHERE для векторной БД

Фильтры в Weaviate работают через indexFilterable=True (отдельный оптимизированный индекс). Они применяются до или после векторного поиска, не влияя на score объектов.

Метод
Тип
Описание
.equal(val)
any
Точное равенство. Для text использует токенизацию.
.not_equal(val)
any
Не равно val.
.greater_than(val)
number / date
Строго больше. Для date: RFC 3339 строка.
.greater_or_equal(val)
number / date
Больше или равно.
.less_than(val)
number / date
Строго меньше.
.less_or_equal(val)
number / date
Меньше или равно.
.like(pattern)
text
Wildcard-паттерн: * — любые символы, ? — один символ.
.contains_any([v1, v2])
text / text[]
Хотя бы один из списка присутствует. Для массивов и одиночных полей.
.contains_all([v1, v2])
text[]
Все значения из списка присутствуют.
.is_none(True)
any
Поле отсутствует или равно null.
.within_geo_range(coord, dist)
GEO_COORDINATES
Объекты в радиусе dist метров от координаты.
from weaviate.classes.query import Filter

with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")

    # ── Простые условия ───────────────────────────────────────────────
    f_published = Filter.by_property("category").equal("AI")
    f_rating    = Filter.by_property("rating").greater_or_equal(4.0)
    f_date      = Filter.by_property("published_at").greater_than("2024-01-01T00:00:00Z")

    # ── Логические операторы ──────────────────────────────────────────
    # AND: оба условия должны выполняться
    f_and = f_published & f_rating

    # OR: хотя бы одно
    f_or = (
        Filter.by_property("category").equal("AI") |
        Filter.by_property("category").equal("ML")
    )

    # NOT: инверсия
    f_not = ~Filter.by_property("category").equal("Spam")

    # all_of / any_of для нескольких условий
    f_complex = Filter.all_of([
        Filter.by_property("rating").greater_than(4.0),
        Filter.by_property("category").equal("AI"),
        Filter.by_property("tags").contains_any(["rag", "llm"]),
    ])

    # ── Wildcard поиск ────────────────────────────────────────────────
    f_like = Filter.by_property("title").like("*Retrieval*")

    # ── Фильтрация через reference (ссылка на другую коллекцию) ──────
    f_ref = (
        Filter.by_ref("hasAuthor")
        .by_property("country")
        .equal("Russia")
    )

    # ── Применение в запросах ─────────────────────────────────────────
    results = articles.query.hybrid(
        query="векторный поиск",
        alpha=0.75,
        where=f_complex,   # ← фильтр прямо в запрос
        limit=10,
    )

    # Fetch с сортировкой (без семантики)
    from weaviate.classes.query import Sort
    results = articles.query.fetch_objects(
        where=f_date,
        sort=Sort.by_property("rating", ascending=False),
        limit=50,
    )

Autocut: автоматическое отсечение по кластерам

Классическое limit=10 возвращает ровно 10 результатов, даже если 7-й по счёту уже нерелевантен. Autocut решает это иначе: он анализирует разрывы в score и автоматически обрезает выдачу на границе кластера.

Как работает autocut: находит скачок score и обрезает
↑ auto_limit=1 обрезает здесь
Кластер A (высокий score) Кластер B Кластер C (низкий)
with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")

    # auto_limit=1 → вернуть только первый кластер
    # auto_limit=2 → первые два кластера
    # Требует RELATIVE_SCORE_FUSION (по умолчанию)
    results = articles.query.hybrid(
        query="retrieval augmented generation",
        alpha=0.75,
        limit=50,        # запрашиваем с запасом
        auto_limit=1,    # но вернём только первый «скачок» релевантности
    )
    print(f"Вернулось {len(results.objects)} объектов из 50 запрошенных")
Autocut + RankedFusion = не работает. Autocut анализирует разрывы в нормализованных score. RankedFusion использует только позиции в ранке (одинаково распределённые), поэтому разрывов нет — autocut всегда вернёт все результаты. Используйте HybridFusion.RELATIVE_SCORE (по умолчанию с v1.24).

Named vectors: несколько пространств на объект

Разные части документа несут разную семантику. Заголовок и тело статьи лучше хранить в разных векторных пространствах: при поиске по заголовку берём title_vec, при поиске по смыслу — content_vec.

from weaviate.classes.config import Configure, Property, DataType

with weaviate.connect_to_local() as client:

    # ── Определяем коллекцию с несколькими векторами ──────────────────
    client.collections.create(
        name="Documents",
        properties=[
            Property(name="title",    data_type=DataType.TEXT),
            Property(name="summary",  data_type=DataType.TEXT),
            Property(name="content",  data_type=DataType.TEXT),
            Property(name="language", data_type=DataType.TEXT, skip_vectorization=True),
        ],
        vector_config={
            # Вектор для заголовка (маленькая модель, быстро)
            "title_vec": Configure.NamedVectors.text2vec_openai(
                source_properties=["title"],           # ← только title
                model="text-embedding-3-small",
            ),
            # Вектор для контента (большая модель, точнее)
            "content_vec": Configure.NamedVectors.text2vec_openai(
                source_properties=["summary", "content"],
                model="text-embedding-3-large",
            ),
        },
    )

    docs = client.collections.use("Documents")

    # ── Запрос по конкретному named vector ────────────────────────────
    # Поиск по заголовку
    results = docs.query.near_text(
        query="введение в machine learning",
        target_vector="title_vec",    # ← используем только этот вектор
        limit=5,
    )

    # Поиск по контенту
    results = docs.query.hybrid(
        query="обучение с подкреплением в robotics",
        target_vector="content_vec",  # ← только content
        alpha=0.75,
        limit=10,
    )

    # ── Multi-target: поиск по нескольким векторам сразу ─────────────
    from weaviate.classes.query import TargetVectors

    results = docs.query.near_text(
        query="machine learning",
        target_vector=TargetVectors.manual_weights({
            "title_vec": 0.3,      # 30% вес на заголовок
            "content_vec": 0.7,    # 70% вес на контент
        }),
        limit=10,
    )

Reranking: переупорядочивание результатов

Hybrid search возвращает хорошие результаты, но не идеальные. Reranker — это вторая модель, которая берёт топ-N результатов и пересортирует их с более глубоким анализом релевантности. Требует настройки reranker-модуля в коллекции.

from weaviate.classes.config import Configure
from weaviate.classes.query import Rerank, MetadataQuery

with weaviate.connect_to_local() as client:

    # ── 1. Создать коллекцию с reranker ───────────────────────────────
    client.collections.create(
        name="ArticlesWithRerank",
        properties=[...],
        vector_config=Configure.Vectors.text2vec_openai(model="text-embedding-3-small"),

        # Cohere reranker (нужен API ключ)
        reranker_config=Configure.Reranker.cohere(
            model="rerank-english-v3.0",
        ),
        # Или локальный cross-encoder:
        # reranker_config=Configure.Reranker.transformers(),
    )

    articles = client.collections.use("ArticlesWithRerank")

    # ── 2. Запрос с reranking ─────────────────────────────────────────
    results = articles.query.hybrid(
        query="best practices for RAG pipelines",
        alpha=0.75,
        limit=50,      # сначала берём 50 кандидатов...

        rerank=Rerank(
            property="content",           # reranker смотрит это поле
            query="RAG optimization techniques",  # можно уточнить запрос
        ),

        return_metadata=MetadataQuery(
            score=True,           # score после fusion
            rerank_score=True,    # score после reranker
        ),
    )

    # Вернётся 50 объектов, пересортированных reranker'ом
    for obj in results.objects[:5]:
        print(obj.properties["title"])
        print(f"  fusion score: {obj.metadata.score:.3f}")
        print(f"  rerank score: {obj.metadata.rerank_score:.3f}")

Generative search: RAG прямо в запросе

Generative search — это встроенный RAG в Weaviate. Не нужно отдельно извлекать документы, формировать промпт и вызывать LLM — всё происходит в одном запросе к базе.

from weaviate.classes.generate import GenerativeSearch

with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")  # нужен generative_config

    # ── Single prompt: свой промпт для каждого результата ─────────────
    results = articles.query.hybrid(
        query="что такое RAG",
        alpha=0.75,
        limit=5,
        return_properties=["title", "content"],

        generate=GenerativeSearch.single(
            # {поле} — подстановка из properties объекта
            prompt="Объясни одним предложением по-русски: {content}"
        ),
    )

    for obj in results.objects:
        print(f"Оригинал: {obj.properties['title']}")
        print(f"Генерация: {obj.generated}")

    # ── Grouped prompt: агрегация всех результатов ────────────────────
    results = articles.query.hybrid(
        query="лучшие практики для RAG",
        alpha=0.75,
        limit=10,
        return_properties=["title", "content"],

        generate=GenerativeSearch.grouped(
            # Все результаты передаются как контекст
            prompt="""На основе этих статей составь краткий список
            ТОП-5 практик для построения RAG-системы. Отвечай по-русски.""",
        ),
    )

    # Групповой ответ — в grouped_task
    print(results.generated)   # итоговый ответ LLM

Контроль возвращаемых данных

from weaviate.classes.query import MetadataQuery

with weaviate.connect_to_local() as client:
    articles = client.collections.use("Articles")

    results = articles.query.hybrid(
        query="...",
        alpha=0.75,
        limit=10,
        offset=20,                          # пагинация

        # Какие свойства вернуть (по умолчанию — все)
        return_properties=["title", "category", "rating"],

        # Какие метаданные включить в ответ
        return_metadata=MetadataQuery(
            uuid=True,           # ID объекта
            score=True,          # итоговый hybrid score
            distance=True,       # дистанция в векторном пространстве
            certainty=True,      # certainty (только cosine)
            explain_score=True,  # подробно: вклад BM25 и vector отдельно
            creation_time=True,  # когда создан
            last_update_time=True,
        ),

        # Включить вектор в ответ (по умолчанию: нет — экономим трафик)
        include_vector=False,
    )

    for obj in results.objects:
        print(obj.uuid)
        print(obj.properties["title"])
        print(f"score={obj.metadata.score:.3f}")
        if obj.metadata.explain_score:
            print(obj.metadata.explain_score)   # «BM25: 0.8, Vector: 0.9»

Multi-tenancy: изоляция данных

Если вы строите SaaS-приложение, где каждый клиент должен видеть только свои документы — multi-tenancy встроен в Weaviate на уровне хранилища. Каждый тенант получает физически изолированные сегменты, а не просто фильтр WHERE.

Collection «KnowledgeBase» (multi-tenancy enabled)
tenant: acme_corp
tenant: globex_inc
tenant: initech
…ещё N тенантов
Каждый тенант → отдельные HNSW граф + BM25 индекс + LSM сегменты
from weaviate.classes.config import Configure
from weaviate.classes.tenants import Tenant, TenantActivityStatus

with weaviate.connect_to_local() as client:

    # ── Создать коллекцию с multi-tenancy ─────────────────────────────
    client.collections.create(
        name="KnowledgeBase",
        properties=[
            Property(name="title",   data_type=DataType.TEXT),
            Property(name="content", data_type=DataType.TEXT),
        ],
        vector_config=Configure.Vectors.text2vec_openai(model="text-embedding-3-small"),
        multi_tenancy_config=Configure.multi_tenancy(
            enabled=True,
            auto_tenant_creation=True,    # создавать тенанта автоматически при вставке
        ),
    )

    kb = client.collections.use("KnowledgeBase")

    # ── Управление тенантами ──────────────────────────────────────────
    # Создать тенантов
    kb.tenants.create(tenants=[
        Tenant(name="acme_corp"),
        Tenant(name="globex_inc"),
        Tenant(name="initech"),
    ])

    # Получить список тенантов
    tenants = kb.tenants.get()
    for t in tenants.values():
        print(f"{t.name}: {t.activity_status}")

    # Деактивировать тенанта (данные остаются, но не доступны)
    kb.tenants.update(tenants=[
        Tenant(name="initech", activity_status=TenantActivityStatus.INACTIVE)
    ])

    # ── Вставка данных в тенанта ──────────────────────────────────────
    acme_kb = kb.with_tenant("acme_corp")   # ← переключаемся на тенанта

    acme_kb.data.insert(properties={
        "title": "Внутренняя документация ACME",
        "content": "...",
    })

    # ── Запрос только в данных тенанта ────────────────────────────────
    results = acme_kb.query.hybrid(
        query="документация по API",
        alpha=0.75,
        limit=10,
    )
    # globex_inc никогда не увидит эти данные

Векторайзеры: автоматическая векторизация

Уникальная особенность Weaviate — не нужно самостоятельно вызывать API эмбеддингов. Достаточно настроить vectorizer при создании коллекции, и Weaviate автоматически векторизует объекты при вставке и запросы при поиске.

text2vec-openai
text-embedding-3-small / 3-large / ada-002
OpenAI API. Нужен OPENAI_APIKEY. Лучшее качество из коробки, платный.
text2vec-cohere
embed-english-v3.0 / multilingual-v3.0
Cohere API. Хороший для многоязычного контента. Платный.
text2vec-ollama
nomic-embed-text / mxbai-embed-large
Локальный Ollama. Бесплатно, приватно, нужен отдельный сервер Ollama.
text2vec-transformers
all-MiniLM-L6-v2 / любая HF модель
HuggingFace модель в Docker-контейнере. Полный контроль, self-hosted.
import weaviate
from weaviate.classes.config import Configure, Property, DataType

# Docker-compose для text2vec-openai:
# weaviate:
#   image: semitechnologies/weaviate:1.26.0
#   environment:
#     ENABLE_MODULES: text2vec-openai
#     OPENAI_APIKEY: sk-...

with weaviate.connect_to_local(
    headers={"X-OpenAI-Api-Key": "sk-..."},  # ключ можно передать в заголовке
) as client:

    client.collections.create(
        name="Articles",
        properties=[
            Property(
                name="title",
                data_type=DataType.TEXT,
                skip_vectorization=False,             # включить в вектор
                vectorize_property_name=False,         # не добавлять "title:" к тексту
            ),
            Property(
                name="internal_id",
                data_type=DataType.TEXT,
                skip_vectorization=True,              # техническое поле — не векторизовать
            ),
        ],
        vector_config=Configure.Vectors.text2vec_openai(
            model="text-embedding-3-small",
            vectorize_collection_name=False,          # не добавлять "Articles " в начало
        ),
    )

    # При вставке — Weaviate сам вызовет OpenAI API
    articles = client.collections.use("Articles")
    articles.data.insert(properties={"title": "Введение в RAG", "internal_id": "ART-001"})

    # При запросе — тоже автоматически
    results = articles.query.near_text(query="документы и контекст", limit=5)

Производительность: HNSW параметры

Weaviate использует HNSW (Hierarchical Navigable Small World) для векторного поиска. Параметры графа влияют на компромисс между скоростью, памятью и точностью поиска.

Параметр
Default
Влияние
ef_construction
128
Качество построения графа. Выше = лучше recall, медленнее индексирование. Изменить нельзя после создания.
max_connections
64
Количество рёбер на узел (аналог M в HNSW). Выше = плотнее граф, больше RAM. Изменить нельзя после создания.
ef
-1 (dynamic)
Размер очереди при поиске. -1 = динамически: limit × dynamic_ef_factor. Меняется в рантайме.
dynamic_ef_min
100
Минимальный ef при динамическом режиме.
dynamic_ef_max
500
Максимальный ef при динамическом режиме.
dynamic_ef_factor
8
ef = limit × factor. limit=10 → ef=80. Больше фактор = точнее, медленнее.
flat_search_cutoff
40000
Если объектов меньше — используется flat (brute-force) поиск вместо HNSW. Точность 100%.
distance_metric
COSINE
COSINE (рекомендован), DOT (быстрее для norm. векторов), L2_SQUARED, HAMMING, MANHATTAN.
from weaviate.classes.config import Configure, VectorDistances

with weaviate.connect_to_local() as client:
    client.collections.create(
        name="ArticlesTuned",
        properties=[...],
        vector_config=Configure.Vectors.text2vec_openai(
            model="text-embedding-3-small",

            # HNSW параметры — задаются при создании коллекции
            vector_index_config=Configure.VectorIndex.hnsw(
                ef_construction=128,      # качество построения
                max_connections=64,       # рёбра в графе (M)
                ef=-1,                    # -1 = динамический ef
                dynamic_ef_min=100,
                dynamic_ef_max=500,
                dynamic_ef_factor=8,      # ef = limit * 8
                flat_search_cutoff=40_000,
                distance_metric=VectorDistances.COSINE,
                quantizer=Configure.VectorIndex.Quantizer.pq(
                    # Product Quantization для сжатия (опционально)
                    segments=96,
                    training_limit=100_000,
                ),
            ),
        ),
    )

    # Изменить ef в рантайме (без пересоздания коллекции)
    collection = client.collections.use("ArticlesTuned")
    collection.config.update(
        vector_config_updates={"": Configure.VectorIndex.hnsw(
            ef=200,    # фиксированный ef для высокой точности
        )}
    )

Scroll API: полный обход коллекции

fetch_objects с offset плохо масштабируется: при offset=100000 база всё равно пробегает 100000 объектов. Для полного обхода используйте cursor-пагинацию через after.

import json
from pathlib import Path

def export_collection(client, collection_name: str, output_file: str, batch: int = 200):
    """Экспортирует все объекты коллекции через cursor-пагинацию."""
    collection = client.collections.use(collection_name)
    path = Path(output_file)

    with path.open("w", encoding="utf-8") as f:
        cursor = None
        total = 0

        while True:
            results = collection.query.fetch_objects(
                limit=batch,
                after=cursor,                   # ← cursor вместо offset
                return_properties=["title", "content", "category"],
                return_metadata=["uuid"],
            )

            if not results.objects:
                break

            for obj in results.objects:
                record = {"uuid": str(obj.uuid), **obj.properties}
                f.write(json.dumps(record, ensure_ascii=False) + "\n")

            cursor = results.objects[-1].uuid  # следующая страница от последнего uuid
            total += len(results.objects)
            print(f"  Экспортировано: {total}")

        print(f"Готово: {total} объектов → {output_file}")


with weaviate.connect_to_local() as client:
    export_collection(client, "Articles", "articles_export.jsonl")

LangChain интеграция

from langchain_weaviate.vectorstores import WeaviateVectorStore
from langchain_openai import OpenAIEmbeddings
import weaviate

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

with weaviate.connect_to_local() as weaviate_client:

    # ── Создать или подключиться к коллекции через LangChain ──────────
    vectorstore = WeaviateVectorStore(
        client=weaviate_client,
        index_name="LangchainArticles",
        text_key="content",
        embedding=embeddings,
        attributes=["title", "category"],  # доп. поля для метаданных
    )

    # ── Добавить документы ────────────────────────────────────────────
    from langchain_core.documents import Document
    docs = [
        Document(page_content="...", metadata={"title": "RAG Guide", "category": "AI"})
    ]
    vectorstore.add_documents(docs)

    # ── Retriever с hybrid search ─────────────────────────────────────
    retriever = vectorstore.as_retriever(
        search_type="mmr",
        search_kwargs={
            "k": 5,
            "fetch_k": 20,         # кандидатов для MMR
            "lambda_mult": 0.6,    # баланс relevance/diversity
        },
    )

    # ── Similarity search с фильтром ──────────────────────────────────
    results = vectorstore.similarity_search(
        query="vector databases comparison",
        k=5,
        where_filter={             # Weaviate-style where filter
            "path": ["category"],
            "operator": "Equal",
            "valueText": "AI",
        },
    )

Типичные ошибки

Ошибка 1: Не закрывать клиент. v4 клиент открывает gRPC-соединение. Если не вызвать client.close() или не использовать with-блок — соединение утечёт.

with weaviate.connect_to_local() as client: ... — всегда.
Ошибка 2: Одиночные вставки в цикле. Каждый data.insert() — отдельный gRPC-запрос. 1000 объектов = 1000 запросов. Используйте data.insert_many(objects) — быстрее в 10–100 раз.
Ошибка 3: Векторизация технических полей. Поля internal_id, status, source_url не несут семантики, но по умолчанию попадают в embedding. Это «загрязняет» вектор и снижает качество поиска. Всегда ставьте skip_vectorization=True для технических полей.
Ошибка 4: RankedFusion с autocut. auto_limit работает только с HybridFusion.RELATIVE_SCORE. С RANKED_FUSION — autocut игнорируется, возвращаются все результаты.
Ошибка 5: Неправильная токенизация для поиска по кодам. WORD-токенизация разбивает «e-commerce» на «e» и «commerce». Для артикулов, кодов, URL используйте Tokenization.FIELD (точное совпадение) или Tokenization.LOWERCASE.
Ошибка 6: Offset-пагинация на больших данных. fetch_objects(offset=100000) — база пробегает 100000 объектов, чтобы их пропустить. При >10000 записей используйте cursor-пагинацию через параметр after=last_uuid.
Ошибка 7: Изменить ef_construction после создания коллекции. ef_construction и max_connections задаются только при создании коллекции — это параметры построения HNSW графа. Для изменения нужно пересоздать коллекцию и переиндексировать данные. Единственный параметр, изменяемый в рантайме — ef.

Шпаргалка

# ── Подключение ───────────────────────────────────────────────────────
with weaviate.connect_to_local() as client:            # Docker
with weaviate.connect_to_weaviate_cloud(url, auth) as: # WCD
with weaviate.connect_to_embedded() as:                # In-process

# ── Коллекции ─────────────────────────────────────────────────────────
client.collections.create(name, properties, vector_config, generative_config)
client.collections.use("Name")         # get reference
client.collections.delete("Name")      # удалить коллекцию
client.collections.list_all()          # все коллекции

# ── CRUD ──────────────────────────────────────────────────────────────
col.data.insert(properties, uuid)
col.data.insert_many([DataObject(...)])  # bulk — всегда!
col.data.update(uuid, properties)        # частичное обновление
col.data.replace(uuid, properties)       # полная замена
col.data.delete_by_id(uuid)
col.data.delete_many(where=Filter...)

# ── Запросы ───────────────────────────────────────────────────────────
col.query.hybrid(query, alpha=0.75, fusion_type, where, limit, auto_limit)
col.query.near_text(query, distance, certainty, target_vector, limit)
col.query.near_vector(vector, distance, limit)
col.query.bm25(query, limit, where)
col.query.fetch_objects(where, sort, limit, after)   # cursor-пагинация

# ── Alpha ────────────────────────────────────────────────────────────
# 0.0 = чисто BM25  |  0.5 = баланс  |  0.75 = рек. для RAG  |  1.0 = чисто vector

# ── Fusion types ──────────────────────────────────────────────────────
HybridFusion.RELATIVE_SCORE   # default v1.24+, нужен для autocut
HybridFusion.RANKED_FUSION    # RRF, без autocut

# ── Фильтры ───────────────────────────────────────────────────────────
Filter.by_property("f").equal(v)
Filter.by_property("f").greater_than(v)
Filter.by_property("f").like("*pattern*")
Filter.by_property("tags").contains_any(["a","b"])
Filter.all_of([f1, f2])   # AND
Filter.any_of([f1, f2])   # OR
f1 & f2   # AND  |  f1 | f2   # OR  |  ~f1   # NOT
Filter.by_ref("ref").by_property("field").equal(v)

# ── Named vectors ─────────────────────────────────────────────────────
col.query.near_text(query, target_vector="vec_name")
TargetVectors.manual_weights({"title_vec": 0.3, "content_vec": 0.7})

# ── Autocut ───────────────────────────────────────────────────────────
col.query.hybrid(query, limit=50, auto_limit=1)  # первый кластер

# ── Reranking ─────────────────────────────────────────────────────────
Rerank(property="content", query="уточнённый запрос")

# ── Generative search ─────────────────────────────────────────────────
GenerativeSearch.single(prompt="Резюмируй: {content}")
GenerativeSearch.grouped(prompt="Объедини эти статьи...")

# ── Metadata ──────────────────────────────────────────────────────────
MetadataQuery(uuid, score, distance, certainty, explain_score, creation_time)

# ── Multi-tenancy ─────────────────────────────────────────────────────
kb.with_tenant("tenant_id").query.hybrid(...)
kb.tenants.create([Tenant("name")])
kb.tenants.update([Tenant("name", activity_status=TenantActivityStatus.INACTIVE)])

# ── HNSW (только при создании, не меняются): ─────────────────────────
# ef_construction=128, max_connections=64
# ef меняется: col.config.update(vector_config_updates={...})

# ── Токенизация (для BM25): ───────────────────────────────────────────
# WORD: статьи  |  FIELD: UUID/коды  |  LOWERCASE: email/url  |  WHITESPACE: акронимы

Практические задания

  1. Hybrid RAG с фильтрами. Создайте коллекцию «TechDocs» со свойствами title, content, language, version (number), tags (text[]). Вставьте 50 документов. Напишите функцию search_docs(query, lang, min_version, tags), которая делает hybrid search (alpha=0.75) с фильтрами по языку, версии и тегам. Добавьте autocut=1 и верните только explain_score.
  2. Named vectors + reranking. Создайте коллекцию с двумя named vectors: title_vec (только title) и body_vec (title + content). Напишите функцию поиска, которая принимает параметр search_by: Literal["title", "body", "both"] и в зависимости от него использует нужный named vector или TargetVectors.manual_weights. Добавьте reranking через Rerank.
  3. Multi-tenant SaaS. Смоделируйте базу знаний для трёх клиентов. Создайте коллекцию с multi-tenancy, добавьте по 20 документов каждому тенанту, реализуйте функцию ask_kb(tenant_id, question), которая делает hybrid search + generative search и возвращает ответ LLM. Проверьте, что тенанты не видят данные друг друга.