Skip to content

Added embedding generation. Avoid creating an instance of EmbeddingFieldFactory manually#3080

Open
mnocon wants to merge 2 commits intoIBX-9846from
IBX-9846-code-samples
Open

Added embedding generation. Avoid creating an instance of EmbeddingFieldFactory manually#3080
mnocon wants to merge 2 commits intoIBX-9846from
IBX-9846-code-samples

Conversation

@mnocon
Copy link
Contributor

@mnocon mnocon commented Mar 11, 2026

No description provided.

@github-actions
Copy link

Preview of modified files

Preview of modified Markdown:

@github-actions
Copy link

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php

docs/search/search_api.md@406:``` php
docs/search/search_api.md@407:// ...
docs/search/search_api.md@408:[[= include_file('code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php', 10, 21) =]]
docs/search/search_api.md@409:// ...
docs/search/search_api.md@410:[[= include_file('code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php', 26, 28) =]]
docs/search/search_api.md@411:// ...
docs/search/search_api.md@412:[[= include_file('code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php', 33, 78) =]]
docs/search/search_api.md@413:```

001⫶// ...
002⫶use Ibexa\Contracts\Core\Repository\SearchService;
003⫶use Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQueryBuilder;
004⫶use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\ContentTypeIdentifier;
005⫶use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchHit;

code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php

docs/search/search_api.md@406:``` php
docs/search/search_api.md@407:// ...
docs/search/search_api.md@408:[[= include_file('code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php', 10, 21) =]]
docs/search/search_api.md@409:// ...
docs/search/search_api.md@410:[[= include_file('code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php', 26, 28) =]]
docs/search/search_api.md@411:// ...
docs/search/search_api.md@412:[[= include_file('code_samples/api/public_php_api/src/Command/FindByTaxonomyEmbeddingCommand.php', 33, 78) =]]
docs/search/search_api.md@413:```

001⫶// ...
002⫶use Ibexa\Contracts\Core\Repository\SearchService;
003⫶use Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQueryBuilder;
004⫶use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\ContentTypeIdentifier;
005⫶use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchHit;
006⫶use Ibexa\Contracts\Taxonomy\Search\Query\Value\TaxonomyEmbedding;
007⫶use Symfony\Component\Console\Attribute\AsCommand;
008⫶use Symfony\Component\Console\Command\Command;
009⫶use Symfony\Component\Console\Input\InputInterface;
010⫶use Symfony\Component\Console\Output\OutputInterface;
011⫶use Symfony\Component\Console\Style\SymfonyStyle;
012⫶
006⫶use Ibexa\Contracts\Core\Search\Embedding\EmbeddingProviderResolverInterface;
007⫶use Ibexa\Contracts\Taxonomy\Search\Query\Value\TaxonomyEmbedding;
008⫶use Symfony\Component\Console\Attribute\AsCommand;
009⫶use Symfony\Component\Console\Command\Command;
010⫶use Symfony\Component\Console\Input\InputInterface;
011⫶use Symfony\Component\Console\Output\OutputInterface;
012⫶use Symfony\Component\Console\Style\SymfonyStyle;
013⫶
014⫶// ...
013⫶
014⫶// ...
015⫶{
016⫶ public function __construct(private readonly SearchService $searchService)
015⫶final class FindByTaxonomyEmbeddingCommand extends Command
016⫶{
017⫶
018⫶// ...
017⫶
018⫶// ...
019⫶        InputInterface $input,
020⫶ OutputInterface $output
021⫶ ): int {
022⫶ $io = new SymfonyStyle($input, $output);
023⫶
024⫶ // Example embedding vector.
025⫶ // In a real-life scenario, generate it with an embedding provider
026⫶ // and make sure its dimensions match the configured model.
027⫶ $vector = [
028⫶ 0.0123,
029⫶ -0.9876,
030⫶ 0.4567,
031⫶ 0.1111,
032⫶ ];
033⫶
034⫶ $query = EmbeddingQueryBuilder::create()
035⫶ ->withEmbedding(new TaxonomyEmbedding($vector))
036⫶ ->setFilter(new ContentTypeIdentifier('article'))
037⫶ ->setLimit(10)
038⫶ ->setOffset(0)
039⫶ ->setPerformCount(true)
040⫶ ->build();
041⫶
042⫶ $result = $this->searchService->findContent($query);
043⫶
044⫶ $io->success(sprintf('Found %d items.', $result->totalCount));
045⫶
046⫶ foreach ($result->searchHits as $searchHit) {
047⫶ assert($searchHit instanceof SearchHit);
048⫶
049⫶ /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */
050⫶ $content = $searchHit->valueObject;
051⫶ $contentInfo = $content->versionInfo->contentInfo;
052⫶
053⫶ $io->writeln(sprintf(
054⫶ '%d: %s',
055⫶ $contentInfo->id,
056⫶ $contentInfo->name
057⫶ ));
058⫶ }
059⫶
060⫶ return self::SUCCESS;
061⫶ }
062⫶}
019⫶    }
020⫶
021⫶ protected function execute(
022⫶ InputInterface $input,
023⫶ OutputInterface $output
024⫶ ): int {
025⫶ $io = new SymfonyStyle($input, $output);
026⫶
027⫶ // Example embedding vector.
028⫶ // In a real-life scenario, generate it with an embedding provider as shown below
029⫶ // and make sure its dimensions match the configured model.
030⫶ $vector = [
031⫶ 0.0123,
032⫶ -0.9876,
033⫶ 0.4567,
034⫶ 0.1111,
035⫶ ];
036⫶
037⫶ $embeddingProvider = $this->embeddingProviderResolver->resolve();
038⫶ $embedding = $embeddingProvider->getEmbedding('example_content');
039⫶
040⫶ $query = EmbeddingQueryBuilder::create()
041⫶ ->withEmbedding(new TaxonomyEmbedding($embedding))
042⫶ ->setFilter(new ContentTypeIdentifier('article'))
043⫶ ->setLimit(10)
044⫶ ->setOffset(0)
045⫶ ->setPerformCount(true)
046⫶ ->build();
047⫶
048⫶ $result = $this->searchService->findContent($query);
049⫶
050⫶ $io->success(sprintf('Found %d items.', $result->totalCount));
051⫶
052⫶ foreach ($result->searchHits as $searchHit) {
053⫶ assert($searchHit instanceof SearchHit);
054⫶
055⫶ /** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */
056⫶ $content = $searchHit->valueObject;
057⫶ $contentInfo = $content->versionInfo->contentInfo;
058⫶
059⫶ $io->writeln(sprintf(
060⫶ '%d: %s',
061⫶ $contentInfo->id,
062⫶ $contentInfo->name
063⫶ ));


code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php

docs/search/search_api.md@417:``` php
docs/search/search_api.md@418:// ...
docs/search/search_api.md@419:[[= include_file('code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php', 10, 15) =]]
docs/search/search_api.md@420:// ...
docs/search/search_api.md@421:[[= include_file('code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php', 16, 18) =]]
docs/search/search_api.md@422:// ...
docs/search/search_api.md@423:[[= include_file('code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php', 25, 36) =]]
docs/search/search_api.md@424:```

001⫶// ...
002⫶use Ibexa\Contracts\Core\Repository\SearchService;
003⫶use Ibexa\Contracts\Core\Repository\Values\Content\Content;
004⫶use Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQueryBuilder;
005⫶use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult;
006⫶use Ibexa\Contracts\Taxonomy\Search\Query\Value\TaxonomyEmbedding;
007⫶
008⫶// ...


code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php

docs/search/search_api.md@417:``` php
docs/search/search_api.md@418:// ...
docs/search/search_api.md@419:[[= include_file('code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php', 10, 15) =]]
docs/search/search_api.md@420:// ...
docs/search/search_api.md@421:[[= include_file('code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php', 16, 18) =]]
docs/search/search_api.md@422:// ...
docs/search/search_api.md@423:[[= include_file('code_samples/api/public_php_api/src/Service/TaxonomyEmbeddingSearchService.php', 25, 36) =]]
docs/search/search_api.md@424:```

001⫶// ...
002⫶use Ibexa\Contracts\Core\Repository\SearchService;
003⫶use Ibexa\Contracts\Core\Repository\Values\Content\Content;
004⫶use Ibexa\Contracts\Core\Repository\Values\Content\EmbeddingQueryBuilder;
005⫶use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult;
006⫶use Ibexa\Contracts\Taxonomy\Search\Query\Value\TaxonomyEmbedding;
007⫶
008⫶// ...
009⫶final class TaxonomyEmbeddingSearchService
009⫶final readonly class TaxonomyEmbeddingSearchService
010⫶{
011⫶
012⫶// ...
013⫶ * @return SearchResult<Content>
014⫶ */
015⫶ public function searchByEmbedding(array $vector): SearchResult
016⫶ {
017⫶ $query = EmbeddingQueryBuilder::create()
018⫶ ->withEmbedding(new TaxonomyEmbedding($vector))
019⫶ ->setLimit(10)
020⫶ ->setOffset(0)
021⫶ ->build();
022⫶
023⫶ return $this->searchService->findContent($query);


code_samples/api/public_php_api/src/embedding_fields.php

010⫶{
011⫶
012⫶// ...
013⫶ * @return SearchResult<Content>
014⫶ */
015⫶ public function searchByEmbedding(array $vector): SearchResult
016⫶ {
017⫶ $query = EmbeddingQueryBuilder::create()
018⫶ ->withEmbedding(new TaxonomyEmbedding($vector))
019⫶ ->setLimit(10)
020⫶ ->setOffset(0)
021⫶ ->build();
022⫶
023⫶ return $this->searchService->findContent($query);


code_samples/api/public_php_api/src/embedding_fields.php

docs/search/search_api.md@467:``` php
docs/search/search_api.md@468:[[= include_file('code_samples/api/public_php_api/src/embedding_fields.php') =]]
docs/search/search_api.md@469:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶// Create an embedding field using the default embedding provider (type derived from configuration's field suffix)
004⫶
005⫶/** @var Ibexa\Contracts\Core\Search\FieldType\EmbeddingFieldFactory $factory */
006⫶$embeddingField = $factory->create();
007⫶echo $embeddingField->getType(); // for example, "ibexa_dense_vector_model_123"
008⫶
009⫶// Create a custom embedding field with a specific type
010⫶$customField = $factory->create('custom_embedding_type');
011⫶echo $customField->getType(); // "custom_embedding_type"


code_samples/back_office/limitation/src/Controller/CustomController.php

docs/permissions/custom_policies.md@253:```php
docs/permissions/custom_policies.md@254:[[= include_file('code_samples/back_office/limitation/src/Controller/CustomController.php') =]]
docs/permissions/custom_policies.md@255:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Controller;
004⫶
005⫶use App\Security\Limitation\CustomLimitationValue;
006⫶use Ibexa\Contracts\AdminUi\Controller\Controller;
007⫶use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
008⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
009⫶use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
010⫶use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
011⫶use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
012⫶use Symfony\Component\HttpFoundation\Request;
013⫶use Symfony\Component\HttpFoundation\Response;
014⫶
015⫶class CustomController extends Controller implements RestrictedControllerInterface
016⫶{
017⫶ use AuthenticatedRememberedCheckTrait {
018⫶ AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
019⫶ }
020⫶
021⫶ public function __construct(
022⫶ // ...,
023⫶ private readonly PermissionResolver $permissionResolver,
024⫶ private readonly PermissionCheckerInterface $permissionChecker
025⫶ ) {
026⫶ }
027⫶
028⫶ // Controller actions...
029⫶ public function customAction(Request $request): Response
030⫶ {
031⫶ // ...
032⫶ if ($this->getCustomLimitationValue()) {
033⫶ // Action only for user having the custom limitation checked
034⫶ }
035⫶
036⫶ return new Response('<html><body>...</body></html>');
037⫶ }
038⫶
039⫶ private function getCustomLimitationValue(): bool
040⫶ {
041⫶ $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2');
042⫶
043⫶ if (is_bool($hasAccess)) {
044⫶ return $hasAccess;
045⫶ }
046⫶
047⫶ $customLimitationValues = $this->permissionChecker->getRestrictions(
048⫶ $hasAccess,
049⫶ CustomLimitationValue::class
050⫶ );
051⫶
052⫶ return $customLimitationValues['value'] ?? false;
053⫶ }
054⫶

code_samples/back_office/limitation/src/Controller/CustomController.php

docs/permissions/custom_policies.md@253:```php
docs/permissions/custom_policies.md@254:[[= include_file('code_samples/back_office/limitation/src/Controller/CustomController.php') =]]
docs/permissions/custom_policies.md@255:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Controller;
004⫶
005⫶use App\Security\Limitation\CustomLimitationValue;
006⫶use Ibexa\Contracts\AdminUi\Controller\Controller;
007⫶use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
008⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
009⫶use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
010⫶use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
011⫶use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
012⫶use Symfony\Component\HttpFoundation\Request;
013⫶use Symfony\Component\HttpFoundation\Response;
014⫶
015⫶class CustomController extends Controller implements RestrictedControllerInterface
016⫶{
017⫶ use AuthenticatedRememberedCheckTrait {
018⫶ AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
019⫶ }
020⫶
021⫶ public function __construct(
022⫶ // ...,
023⫶ private readonly PermissionResolver $permissionResolver,
024⫶ private readonly PermissionCheckerInterface $permissionChecker
025⫶ ) {
026⫶ }
027⫶
028⫶ // Controller actions...
029⫶ public function customAction(Request $request): Response
030⫶ {
031⫶ // ...
032⫶ if ($this->getCustomLimitationValue()) {
033⫶ // Action only for user having the custom limitation checked
034⫶ }
035⫶
036⫶ return new Response('<html><body>...</body></html>');
037⫶ }
038⫶
039⫶ private function getCustomLimitationValue(): bool
040⫶ {
041⫶ $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2');
042⫶
043⫶ if (is_bool($hasAccess)) {
044⫶ return $hasAccess;
045⫶ }
046⫶
047⫶ $customLimitationValues = $this->permissionChecker->getRestrictions(
048⫶ $hasAccess,
049⫶ CustomLimitationValue::class
050⫶ );
051⫶
052⫶ return $customLimitationValues['value'] ?? false;
053⫶ }
054⫶
055⫶    public function performAccessCheck(): void
056⫶ {
057⫶ $this->traitPerformAccessCheck();
058⫶ $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
059⫶ }
060⫶}
055⫶    #[\Override]
056⫶ public function performAccessCheck(): void
057⫶ {
058⫶ $this->traitPerformAccessCheck();
059⫶ $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
060⫶ }
061⫶}


Download colorized diff

@sonarqubecloud
Copy link

0.1111,
];

$embeddingProvider = $this->embeddingProviderResolver->resolve();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the vector defined in line 45 and embedding generated by the provider there are two contradicting ways of getting the vector. But vector is not used anymore in the command, so... how about I remove it completely?

Copy link
Contributor

@dabrt dabrt Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what if the command example called the service to perform the searchByEmbedding function?
This way both the command and the service could stay in the example - the command would show how to obtain the embedding and call the service to find it, the service would call the query?

But... where would I want to put the filtering - command or the service? I guess the service?

use Ibexa\Contracts\Taxonomy\Search\Query\Value\TaxonomyEmbedding;

final class TaxonomyEmbeddingSearchService
final readonly class TaxonomyEmbeddingSearchService
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You suggested removing the service altogether - do you think I should keep it?

How about the two samples stay but i change their order, so that the service is presented first and then...
The command shows how it can be used?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants