@@ -748,6 +748,99 @@ public function bulkInsert(array $values): array
748748 return $ this ->processor ->processBulkInsert ($ this , $ this ->connection ->insert ($ this ->grammar ->compileInsert ($ this , $ values ), [], true ));
749749 }
750750
751+ /**
752+ * Insert or update records matching the unique key.
753+ *
754+ * ES has no unique column constraints, so this queries for existing
755+ * documents first, then issues a single bulk request mixing index
756+ * (for new docs) and update (for existing docs) actions.
757+ */
758+ public function upsert (array $ values , $ uniqueBy , $ update = null ): int
759+ {
760+ // Normalize to batch format
761+ if (! array_is_list ($ values )) {
762+ $ values = [$ values ];
763+ }
764+
765+ $ uniqueBy = (array ) $ uniqueBy ;
766+
767+ // Empty update means plain insert
768+ if ($ update === []) {
769+ $ this ->insert ($ values );
770+
771+ return count ($ values );
772+ }
773+
774+ // Collect the unique field values from the input
775+ $ lookupValues = [];
776+ foreach ($ values as &$ doc ) {
777+ $ key = $ this ->buildUpsertKey ($ doc , $ uniqueBy );
778+ $ doc ['_upsert_key ' ] = $ key ;
779+ $ lookupValues [] = $ key ;
780+ }
781+ unset($ doc );
782+
783+ // Query ES for existing documents matching the unique fields
784+ $ existingIds = $ this ->findExistingIds ($ uniqueBy , $ lookupValues );
785+
786+ // Compile and execute the bulk request
787+ $ dsl = $ this ->grammar ->compileUpsert ($ this , $ values , $ existingIds , $ update );
788+ $ result = $ this ->connection ->insert ($ dsl );
789+
790+ return $ this ->processor ->processUpsert ($ this , $ result );
791+ }
792+
793+ /**
794+ * Build a lookup key from the document's unique field values.
795+ */
796+ private function buildUpsertKey (array $ doc , array $ uniqueBy ): string
797+ {
798+ $ parts = [];
799+ foreach ($ uniqueBy as $ field ) {
800+ $ parts [] = (string ) ($ doc [$ field ] ?? '' );
801+ }
802+
803+ return implode ('| ' , $ parts );
804+ }
805+
806+ /**
807+ * Find existing document IDs by unique field values.
808+ *
809+ * Returns a map of lookup_key => _id.
810+ */
811+ private function findExistingIds (array $ uniqueBy , array $ lookupValues ): array
812+ {
813+ // Build a fresh query against the same index
814+ $ query = $ this ->newQuery ();
815+
816+ if (count ($ uniqueBy ) === 1 ) {
817+ // Single field: simple whereIn
818+ $ field = $ uniqueBy [0 ];
819+ $ fieldValues = array_unique (array_map (fn ($ key ) => explode ('| ' , $ key )[0 ], $ lookupValues ));
820+ $ results = $ query ->whereIn ($ field , $ fieldValues )->get ();
821+ } else {
822+ // Multi-field: use bool should with exact match per combination
823+ foreach (array_unique ($ lookupValues ) as $ key ) {
824+ $ parts = explode ('| ' , $ key );
825+ $ query ->orWhere (function ($ q ) use ($ uniqueBy , $ parts ) {
826+ foreach ($ uniqueBy as $ i => $ field ) {
827+ $ q ->where ($ field , $ parts [$ i ] ?? '' );
828+ }
829+ });
830+ }
831+ $ results = $ query ->get ();
832+ }
833+
834+ // Map lookup_key => _id
835+ $ map = [];
836+ foreach ($ results as $ result ) {
837+ $ key = $ this ->buildUpsertKey ((array ) $ result , $ uniqueBy );
838+ $ map [$ key ] = $ result ['_id ' ];
839+ }
840+
841+ return $ map ;
842+ }
843+
751844 /**
752845 * {@inheritdoc}
753846 */
0 commit comments