1+ <?php
2+
3+ namespace Vladvildanov \PredisVl \Index ;
4+
5+ use Exception ;
6+ use Predis \Client ;
7+ use Predis \Command \Argument \Search \SchemaFields \VectorField ;
8+ use Predis \Response \ServerException ;
9+ use Vladvildanov \PredisVl \Enum \SearchField ;
10+ use Vladvildanov \PredisVl \Enum \StorageType ;
11+ use Vladvildanov \PredisVl \Factory ;
12+ use Vladvildanov \PredisVl \FactoryInterface ;
13+
14+ class SearchIndex implements IndexInterface
15+ {
16+ /**
17+ * @var array
18+ */
19+ protected array $ schema ;
20+
21+ /**
22+ * @var FactoryInterface
23+ */
24+ protected FactoryInterface $ factory ;
25+
26+ public function __construct (protected Client $ client , array $ schema , FactoryInterface $ factory = null )
27+ {
28+ $ this ->validateSchema ($ schema );
29+ $ this ->factory = $ factory ?? new Factory ();
30+ }
31+
32+ /**
33+ * @inheritDoc
34+ */
35+ public function getSchema (): array
36+ {
37+ return $ this ->schema ;
38+ }
39+
40+ /**
41+ * @inheritDoc
42+ */
43+ public function create (bool $ isOverwrite = false ): bool
44+ {
45+ if ($ isOverwrite ) {
46+ try {
47+ $ this ->client ->ftdropindex ($ this ->schema ['index ' ]['name ' ]);
48+ } catch (ServerException $ exception ) {
49+ // Do nothing on exception, there's no way to check if index already exists.
50+ }
51+ }
52+
53+ $ createArguments = $ this ->factory ->createIndexBuilder ();
54+
55+ if (array_key_exists ('storage_type ' , $ this ->schema ['index ' ])) {
56+ $ createArguments = $ createArguments ->on (
57+ StorageType::from (strtoupper ($ this ->schema ['index ' ]['storage_type ' ]))->value
58+ );
59+ } else {
60+ $ createArguments = $ createArguments ->on ();
61+ }
62+
63+ if (array_key_exists ('prefix ' , $ this ->schema ['index ' ])) {
64+ $ createArguments = $ createArguments ->prefix ([$ this ->schema ['index ' ]['prefix ' ]]);
65+ }
66+
67+ $ schema = [];
68+
69+ foreach ($ this ->schema ['fields ' ] as $ fieldName => $ fieldData ) {
70+ $ fieldEnum = SearchField::fromName ($ fieldData ['type ' ]);
71+
72+ if (array_key_exists ('alias ' , $ fieldData )) {
73+ $ alias = $ fieldData ['alias ' ];
74+ } else {
75+ $ alias = '' ;
76+ }
77+
78+ if ($ fieldEnum === SearchField::vector) {
79+ $ schema [] = $ this ->createVectorField ($ fieldName , $ alias , $ fieldData );
80+ } else {
81+ $ fieldClass = $ fieldEnum ->fieldMapping ();
82+ $ schema [] = new $ fieldClass ($ fieldName , $ alias );
83+ }
84+ }
85+
86+ $ response = $ this ->client ->ftcreate ($ this ->schema ['index ' ]['name ' ], $ schema , $ createArguments );
87+
88+ return $ response == 'OK ' ;
89+ }
90+
91+ /**
92+ * Loads data into current index.
93+ * Accepts array for hashes and string for JSON type.
94+ */
95+ public function load (string $ key , mixed $ values ): bool
96+ {
97+ if (is_string ($ values )) {
98+ $ response = $ this ->client ->jsonset ($ key , '$ ' , $ values );
99+ } elseif (is_array ($ values )) {
100+ $ response = $ this ->client ->hmset ($ key , $ values );
101+ }
102+
103+ return $ response == 'OK ' ;
104+ }
105+
106+ /**
107+ * @inheritDoc
108+ */
109+ public function fetch (string $ id ): mixed
110+ {
111+ $ key = (array_key_exists ('prefix ' , $ this ->schema ['index ' ]))
112+ ? $ this ->schema ['index ' ]['prefix ' ] . $ id
113+ : $ id ;
114+
115+ if (
116+ array_key_exists ('storage_type ' , $ this ->schema ['index ' ])
117+ && StorageType::from (strtoupper ($ this ->schema ['index ' ]['storage_type ' ])) === StorageType::json
118+ ) {
119+ return $ this ->client ->jsonget ($ key );
120+ }
121+
122+ return $ this ->client ->hgetall ($ key );
123+ }
124+
125+ /**
126+ * Validates schema array.
127+ *
128+ * @param array $schema
129+ * @return void
130+ * @throws Exception
131+ */
132+ protected function validateSchema (array $ schema ): void
133+ {
134+ if (!array_key_exists ('index ' , $ schema )) {
135+ throw new Exception ("Schema should contains 'index' entry. " );
136+ }
137+
138+ if (!array_key_exists ('name ' , $ schema ['index ' ])) {
139+ throw new Exception ("Index name is required. " );
140+ }
141+
142+ if (
143+ array_key_exists ('storage_type ' , $ schema ['index ' ]) &&
144+ null === StorageType::tryFrom (strtoupper ($ schema ['index ' ]['storage_type ' ]))
145+ ) {
146+ throw new Exception ('Invalid storage type value. ' );
147+ }
148+
149+ if (!array_key_exists ('fields ' , $ schema )) {
150+ throw new Exception ('Schema should contains at least one field. ' );
151+ }
152+
153+ foreach ($ schema ['fields ' ] as $ fieldData ) {
154+ if (!array_key_exists ('type ' , $ fieldData )) {
155+ throw new Exception ('Field type should be specified for each field. ' );
156+ }
157+
158+ if (!in_array ($ fieldData ['type ' ], SearchField::names (), true )) {
159+ throw new Exception ('Invalid field type. ' );
160+ }
161+ }
162+
163+ $ this ->schema = $ schema ;
164+ }
165+
166+ /**
167+ * Creates a Vector field from given configuration.
168+ *
169+ * @param string $fieldName
170+ * @param string $alias
171+ * @param array $fieldData
172+ * @return VectorField
173+ * @throws Exception
174+ */
175+ protected function createVectorField (string $ fieldName , string $ alias , array $ fieldData ): VectorField
176+ {
177+ $ mandatoryKeys = ['datatype ' , 'dims ' , 'distance_metric ' , 'algorithm ' ];
178+ $ intersections = array_intersect ($ mandatoryKeys , array_keys ($ fieldData ));
179+
180+ if (count ($ intersections ) !== count ($ mandatoryKeys )) {
181+ throw new Exception ("datatype, dims, distance_metric and algorithm are mandatory parameters for vector field. " );
182+ }
183+
184+ return new VectorField (
185+ $ fieldName ,
186+ strtoupper ($ fieldData ['algorithm ' ]),
187+ [
188+ 'TYPE ' , strtoupper ($ fieldData ['datatype ' ]),
189+ 'DIM ' , strtoupper ($ fieldData ['dims ' ]),
190+ 'DISTANCE_METRIC ' , strtoupper ($ fieldData ['distance_metric ' ])
191+ ],
192+ $ alias
193+ );
194+ }
195+ }
0 commit comments