定制 micka-17/typesense-bundle 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

micka-17/typesense-bundle

最新稳定版本:v2.0.5

Composer 安装命令:

composer require micka-17/typesense-bundle

包简介

Un bundle Symfony pour intégrer Typesense.

README 文档

README

Symfony bundle for Typesense — full-text search, vector search, and AI-powered search for your Doctrine entities.

PHP Symfony Typesense License

Compatibility

Bundle PHP Symfony Typesense Server typesense-php
2.x ≥8.5 ^7.4 | ^8.0 ≥30 ^6.0
1.x ≥8.1 ^6.4 | ^7.0 26–29 ^5.0

Installation

composer require micka-17/typesense-bundle

Register the bundle (if not using Symfony Flex):

// config/bundles.php
return [
    Micka17\TypesenseBundle\TypesenseBundle::class => ['all' => true],
];

Quick Start

1. Configure the bundle

# config/packages/typesense.yaml
typesense:
    api_key: '%env(TYPESENSE_API_KEY)%'
    cluster:
        nodes:
            - { host: '%env(TYPESENSE_HOST)%', port: 8108, protocol: http }

    indexable_entities:
        - App\Entity\Product
        - App\Entity\Category

    auto_update: true   # or: {enabled: true, mode: sync|async}
# .env
TYPESENSE_API_KEY=xyz
TYPESENSE_HOST=localhost

2. Annotate your entities

use Micka17\TypesenseBundle\Attribute\TypesenseIndexable;
use Micka17\TypesenseBundle\Attribute\TypesenseField;

#[TypesenseIndexable(collection: 'products')]
class Product
{
    #[TypesenseField(type: 'string', sort: true)]
    private string $name;

    #[TypesenseField(type: 'float', facet: true)]
    private float $price;

    #[TypesenseField(type: 'string[]', facet: true)]
    private array $tags = [];

    #[TypesenseField(type: 'int32')]
    private int $stock;
}

3. Create collections and index data

php bin/console micka17:typesense:sync

4. Search

use Micka17\TypesenseBundle\Service\FinderService;

class ProductController
{
    public function __construct(private readonly FinderService $finder) {}

    public function search(string $q): array
    {
        $result = $this->finder->search('products', [
            'q'        => $q,
            'query_by' => 'name',
            'filter_by' => 'stock:>0',
        ]);

        return $result->hits; // array of documents
    }
}

Configuration Reference

typesense:
    api_key: '%env(TYPESENSE_API_KEY)%'

    # --- Cluster ---
    cluster:
        enabled: false                  # true for multi-node HA setup
        nodes:
            - { host: localhost, port: 8108, protocol: http }
        read_preference: nearest        # nearest | leader | follower
        consistency_level: eventual     # eventual | strong

    # --- Entities to index ---
    indexable_entities:
        - App\Entity\Product
        - App\Entity\Category

    # --- Auto-index on Doctrine events ---
    auto_update: true

    # --- Error tracking ---
    error_tracking:
        enabled: true
        log_level: error
        track_node_errors: false
        node_error_fields: [host, port, error_message]

    # ── V2 Resources (Typesense 30+) ──────────────────────────────────────

    synonym_sets:
        main:
            items:
                electronics:
                    synonyms: [phone, mobile, smartphone]
                size:
                    root: large
                    synonyms: [big, huge, xl, xxl]

    curation_sets:
        featured:
            items:
                promote-iphone:
                    rule: { query: iphone }
                    includes:
                        - { id: 'product-42', position: 1 }

    presets:
        product_default:
            value:
                query_by: name,description
                per_page: 20
                sort_by: _text_match:desc

    stemming_dictionaries:
        french:
            words:
                - { word: chaussures, root: chaussure }
                - { word: couraient, root: courir }

    analytics_rules:
        popular_products:
            type: popular_queries
            params:
                source:
                    collections: [products]
                destination:
                    collection: popular_queries

    nl_search_models:
        products-nl:
            model_name: openai/gpt-4o-mini
            api_key: '%env(OPENAI_API_KEY)%'
            system_prompt: 'You translate natural language into Typesense search parameters.'

    conversation_models:
        support-bot:
            model_name: openai/gpt-4o-mini
            api_key: '%env(OPENAI_API_KEY)%'
            system_prompt: 'You are a helpful product assistant.'

    # --- Legacy (V1 only, migrate to synonym_sets) ---
    # synonyms: []

Attributes

#[TypesenseIndexable]

Applied on the entity class. Defines the Typesense collection.

Parameter Type Default Description
collection string Collection name in Typesense
defaultSortingField string Field used for default sort (must be numeric)
normalizerMethod string Custom method on the entity to build the document
metadata array [] Arbitrary metadata attached to the collection
options array [] Extra collection-level options (voice_query_model…)

#[TypesenseField]

Applied on entity properties. Maps a property to a Typesense field.

Parameter Type Default Description
type string auto Typesense field type (string, int32, float, bool, string[], float[], auto, …)
name string Override the field name in the index
facet bool false Enable faceting
sort bool false Enable sorting
optional bool false Allow null values
index bool true Include field in the index
getter string Method name to call instead of reading the property
reference string JOIN reference, format collection.field
asyncReference bool false Async JOIN reference
cascadeDelete bool false Delete related document on parent delete
embed array Auto-embedding config (requires type: auto)
numDim int Vector dimension (required for float[] fields)
vecDist string Vector distance metric: cosine, ip, l2sq
hnswParams array HNSW index parameters
truncate int Max characters (string fields only)
tokenSeparators string[] [] Custom token separators
symbolsToIndex string[] [] Symbols to include in the index

Examples:

// Vector field with auto-embedding
#[TypesenseField(
    type: 'float[]',
    embed: ['from' => ['name', 'description'], 'model_config' => ['model_name' => 'ts/e5-small']],
    numDim: 384,
    vecDist: 'cosine',
)]
private array $embedding;

// JOIN reference
#[TypesenseField(type: 'string', reference: 'brands.id')]
private string $brandId;

// Custom getter
#[TypesenseField(type: 'string[]', getter: 'getTagNames')]
private Collection $tags;

FinderService

Inject Micka17\TypesenseBundle\Service\FinderService and use these methods:

search(string $collection, array $params): Result

$result = $finder->search('products', ['q' => 'laptop', 'query_by' => 'name']);
$result->found      // int: total matches
$result->hits       // array of documents
$result->tookMs     // int: query time
$result->facetCounts // array of facet buckets

searchAndPaginate(string $collection, array $params, int $page, int $perPage): Paginator

$paginator = $finder->searchAndPaginate('products', ['q' => 'laptop'], page: 2, perPage: 15);
$paginator->items        // array of documents
$paginator->total        // int
$paginator->currentPage  // int
$paginator->lastPage     // int (virtual, via property hook)
$paginator->hasNextPage  // bool
$paginator->nextPage     // ?int

multiSearch(array $searchRequests): Result[]

$results = $finder->multiSearch([
    'searches' => [
        ['collection' => 'products', 'q' => 'laptop', 'query_by' => 'name'],
        ['collection' => 'categories', 'q' => 'laptop', 'query_by' => 'name'],
    ],
]);

searchWithPreset(string $presetName, array $extra = []): Result

$result = $finder->searchWithPreset('product_default');

unionSearch(array $searches, array $commonParams = []): Result (v30+)

Searches multiple collections and merges results into a single ranked list. Each hit includes _collection.

$result = $finder->unionSearch([
    ['collection' => 'books',   'q' => 'harry potter', 'query_by' => 'title'],
    ['collection' => 'movies',  'q' => 'harry potter', 'query_by' => 'title'],
], ['per_page' => 10]);

searchWithDiversification(string $collection, array $params, float $mmrLambda = 0.5, ?string $mmrEmbeddingField = null): Result (v30+)

MMR (Maximal Marginal Relevance) diversification — reduces duplicate results. mmrLambda = 1.0 → max relevance, 0.0 → max diversity.

$result = $finder->searchWithDiversification(
    'products',
    ['q' => 'laptop', 'vector_query' => 'embedding:([0.1, ...])'],
    mmrLambda: 0.7,
    mmrEmbeddingField: 'embedding',
);

conversationalSearch(string $collection, array $params, string $modelId, ?string $conversationId = null): Result (v30+)

RAG-style search. The model answers the query and the answer is in $result->conversationAnswer.

$result = $finder->conversationalSearch('products', ['q' => 'best laptop for gaming'], 'support-bot');
echo $result->conversationAnswer;   // "Based on our catalog, the ASUS ROG..."
echo $result->conversationId;       // "conv-abc123" — pass on follow-ups

// Follow-up:
$result2 = $finder->conversationalSearch('products', ['q' => 'what about battery life?'], 'support-bot', $result->conversationId);

naturalLanguageSearch(string $collection, string $query, string $modelId, array $extra = []): Result (v30+)

Translates a free-text question into structured search parameters server-side.

$result = $finder->naturalLanguageSearch(
    'products',
    'laptops under 1000 euros with good battery',
    'products-nl',
    ['per_page' => 5],
);

Commands

Sync & maintenance

# Create/update collections + apply all V2 resources in one shot
php bin/console micka17:typesense:sync

# Diagnose configuration vs Typesense state
php bin/console micka17:typesense:doctor

# Re-index a specific entity (paginated batches, OOM-safe)
php bin/console typesense:reindex "App\Entity\Product"
php bin/console typesense:reindex "App\Entity\Product" --batch-size=500

# Export documents as JSONL
php bin/console micka17:typesense:documents:export products
php bin/console micka17:typesense:documents:export products --output=/tmp/export.jsonl --filter-by="stock:>0"

# Update Typesense server config dynamically
php bin/console micka17:typesense:config:update cache-num-entries=1000

# Migrate V1 config → V2 YAML (dry-run, generates output)
php bin/console micka17:typesense:migrate-config
php bin/console micka17:typesense:migrate-config --output=config/packages/typesense_v2.yaml

Schema diff / ALTER

Compare the live Typesense collection schema against the PHP attribute schema without recreating the collection.

# Show what would change
php bin/console micka17:typesense:schema:diff "App\Entity\Product"

# Apply non-destructive changes (add/drop fields) via PATCH
php bin/console micka17:typesense:schema:diff "App\Entity\Product" --apply

# Force full recreation when field types changed (all documents will be lost)
php bin/console micka17:typesense:schema:diff "App\Entity\Product" --force-recreate

The command exits with code 1 (and explains why) when the diff contains field type conflicts or collection-level metadata changes — those require --force-recreate.

API Keys

# List all keys (values are never shown after creation)
php bin/console micka17:typesense:keys:list

# Create a key — the value is shown ONCE, copy it immediately
php bin/console micka17:typesense:keys:create \
  --description="Search only" \
  --actions=documents:search \
  --collections=products

# Retrieve metadata for a key by ID
php bin/console micka17:typesense:keys:retrieve 42

# Delete a key (immediately revokes access)
php bin/console micka17:typesense:keys:delete 42 --yes

PHP:

use Micka17\TypesenseBundle\Service\KeysManager;

// Create — returns ['id' => ..., 'value' => 'secret...'] (value only at creation)
$key = $keysManager->createKey([
    'description' => 'Search only',
    'actions'     => ['documents:search'],
    'collections' => ['products'],
]);

$keysManager->listKeys();        // ['keys' => [...]]
$keysManager->retrieveKey(42);   // ['id' => 42, 'description' => ...]
$keysManager->deleteKey(42);

Aliases (zero-downtime reindex)

# Create or update an alias
php bin/console micka17:typesense:aliases:upsert products products_v2

# List all aliases
php bin/console micka17:typesense:aliases:list

# Delete an alias
php bin/console micka17:typesense:aliases:delete products

PHP — atomic swap:

use Micka17\TypesenseBundle\Service\AliasManager;

$previous = $aliasManager->swapAlias('products', 'products_v2');
// $previous = 'products_v1' — the old collection, safe to delete

See docs/guides/01-zero-downtime-reindex.md for the full workflow.

Resources

php bin/console micka17:typesense:synonym-sets:apply
php bin/console micka17:typesense:presets:apply
php bin/console micka17:typesense:stemming:apply
php bin/console micka17:typesense:analytics:rules:apply
php bin/console micka17:typesense:nl-search-models:apply
php bin/console micka17:typesense:conversation-models:apply
php bin/console micka17:typesense:curation-sets:apply

# List resources
php bin/console micka17:typesense:presets:list
php bin/console micka17:typesense:analytics:rules:list
# … (one :list command per resource type)

# Delete a resource
php bin/console micka17:typesense:presets:delete my-preset
php bin/console micka17:typesense:stemming:delete fr
# … (one :delete command per resource type)

# Import a stemming dictionary from file (.json or .csv)
php bin/console micka17:typesense:stemming:import fr-verbs /path/to/dict.csv

Admin Dashboard

The bundle ships with a Bootstrap 5 admin dashboard (no Webpack required) accessible at /admin/typesense/.

It covers all resource types: Collections, Presets, Synonym Sets, Curation Sets, Stemming Dictionaries, Analytics Rules, NL Search Models, Conversation Models.

To enable it, ensure the bundle routes are imported:

# config/routes.yaml
typesense_admin:
    resource: '@TypesenseBundle/config/routes.yaml'

Protect the prefix in your firewall as needed:

# config/packages/security.yaml
access_control:
    - { path: ^/admin/typesense, roles: ROLE_ADMIN }

Async Indexing (Symfony Messenger)

By default, Doctrine events (persist/update/remove) index documents synchronously in the same request. For large documents or high-traffic apps, switch to async mode using Symfony Messenger.

1. Enable async mode

# config/packages/typesense.yaml
typesense:
    auto_update:
        enabled: true
        mode: async   # dispatches IndexDocumentMessage / DeleteDocumentMessage

2. Route the messages to a transport

# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async: '%env(MESSENGER_TRANSPORT_DSN)%'
        routing:
            Micka17\TypesenseBundle\Messenger\IndexDocumentMessage: async
            Micka17\TypesenseBundle\Messenger\DeleteDocumentMessage: async

3. Run the worker

php bin/console messenger:consume async --time-limit=3600

How it works

Event Message dispatched
postPersist / postUpdate IndexDocumentMessage(entityClass, entityId)
preRemove DeleteDocumentMessage(collectionName, documentId)

The handler re-fetches the entity from the database before normalizing, so you always index the freshest data. If the entity was deleted between dispatch and handling, the handler silently skips it.

Note: auto_update: true (the default) is equivalent to {enabled: true, mode: sync} — no migration needed.

V1 → V2 Migration Guide

Run php bin/console micka17:typesense:migrate-config to detect issues automatically.

Breaking changes

Area V1 V2
PHP ≥8.1 ≥8.5
Typesense Server 26–29 ≥30
typesense-php ^5.0 ^6.0
Global synonyms typesense.synonyms typesense.synonym_sets
Client property $client->synonyms $client->synonymSets
Result getters (getFound(), getHits()) public readonly properties ($result->found, $result->hits)
Paginator getters (getLastPage()) virtual property hooks ($paginator->lastPage)

Step 1 — Upgrade PHP and dependencies

# Requires PHP 8.5+
composer require micka-17/typesense-bundle:^2.0

Step 2 — Migrate global synonyms

Before (V1):

typesense:
    synonyms:
        - { id: size-synonyms, synonyms: [large, big, huge] }

After (V2):

typesense:
    synonym_sets:
        my-global-set:
            items:
                size-synonyms:
                    synonyms: [large, big, huge]

Step 3 — Update API key scopes

If your Typesense API keys include synonyms:*, replace with synonym_sets:*.

Step 4 — Update Result / Paginator usage

// V1
$result->getFound();
$result->getHits();
$paginator->getLastPage();
$paginator->getItems();

// V2
$result->found;
$result->hits;
$paginator->lastPage;
$paginator->items;

Step 5 — Migrate collection overrides to curation_sets

Collection-level overrides (/collections/{name}/overrides) still work in Typesense 30 but the new recommended approach uses global curation_sets. Migrate progressively:

typesense:
    curation_sets:
        my-set:
            items:
                promote-best:
                    rule: { query: laptop }
                    includes:
                        - { id: 'product-1', position: 1 }

Step 6 — Re-sync

php bin/console micka17:typesense:sync
php bin/console micka17:typesense:doctor

Advanced Guides

Guide Description
Zero-downtime reindex Alias workflow — swap collections without search interruption
Vector search & auto-embedding Built-in models, OpenAI, hybrid search, MMR diversification
Conversational RAG search Multi-turn chat with LLM-generated answers from your data
Analytics pipeline Track queries, clicks, and build a popular-searches feed
Search-as-you-type Autocomplete UI with a Symfony JSON endpoint (~20 lines JS)
Search parameters reference All Typesense search parameters including V30+ additions

Contributing

Pull requests are welcome. Please run the test suite and PHPStan before submitting:

composer test      # vendor/bin/phpunit
composer phpstan   # vendor/bin/phpstan analyse --memory-limit=512M

All tests must pass and PHPStan level 6 must report 0 errors.

License

MIT — see LICENSE.

统计信息

  • 总下载量: 43
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-07-03

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固