Skip to content

Conversation

@will-hwang
Copy link
Contributor

@will-hwang will-hwang commented Feb 19, 2025

Description

This PR implements optimized text embedding processor for single document update scenario.

Proposed State [Single Document Update]

proposed-flow

Steps:

  1. Process Map is generated based on IngestDocument and Field Map defined.
  2. If ignore_existing feature is set to true, filter the process map.
    1. Existing document is fetched via OpenSearch client’s Get Action to compare the existing inference text against IngestDocument
      1. if document does not exist, or any exception is thrown, fallback to calling model inference
    2. Locate the embedding fields in the existing document
      1. Recursively traverse the process map to find embedding fields. Keeping track of the traversal path is required for look up in existing document.
      2. Once embedding fields are found, attempt to copy embeddings from existing document to ingest document.
    3. If eligible, copy over the vector embeddings from existing document to ingest document
      1. It is eligible for copy if inference text in ingest document and existing document is the same, and embeddings for the inference text exist in the existing document.
      2. Note, if in case of values in list, the fields in the same index are compared to determine text equality
    4. If eligible fields have been copied, remove the entry in process map
  3. Inference List is generated based on entries in Filtered Process Map.
  4. ML Commons InferenceSentence API is invoked with filtered inference list.
  5. Embeddings for filtered inference list are generated in ML Commons.
  6. Embeddings for filtered inference list are mapped to target fields via entries defined in process map.
  7. Embeddings for filtered inference list are populated to target fields in IngestDocument

User Scenarios

Scenario 1: Document Update with no change to all fields

Update Document
ID: 1
"category": [
    {
        "name": {
            "en": "this is 1st name"
        }
    },
    {
        "name": {
            "en": "this is 2nd name"
        }
    }
]
Existing Document
ID: 1
"category": [
    {
        "name": {
            "category_name_vector": [
                -0.1,
                -0.1,
                -0.1,
                ...
            ],
            "en": "this is 1st name"
        }
        
    },
    {
        "name": {
            "category_name_vector": [
                -0.2,
               -0.2,
               -0.2,
                ...
            ],
            "en": "this is 2nd name"
        }
    }
]
Filtered Process Map 
{
  "category":` `
        {
            "name": {
                "category_name_vector": [~~"this is 1st name", "this is 2nd name"~~]
            }
        }
}
Filtered Inference List
[]
Ingested Document
"category": [
    {
        "name": {
            "category_name_vector": [ // copied from existing document
                -0.1,
                -0.1,
                -0.1,
                ...
            ],
            "en": "this is 1st name"
        }
        
    },
    {
        "name": {
            "category_name_vector": [ // copied from existing document
                -0.2,
                -0.2,
                -0.2,
                ...
            ],
            "en": "this is 2nd name"
        }
    }
]

Explanation: Both inference texts “this is 1st name”, “this is 2nd name” mapped with embedding have remained unchanged, so the corresponding embedding values have been copied over, instead of making inference calls

Scenario 2: Document Update with partial change to fields

Update Document
ID: 1
"category": [
   {
        "name": {
            "en": **"this is 2nd name"**
        }
   },
    {
        "name": {
            "en": "this is 2nd name"
        }
    }
]
Existing Document
ID: 1
"category": [
    {
        "name": {
            "category_name_vector": [
                -0.1,
                -0.1,
                -0.1,
                ...
            ],
            "en": "this is 1st name"
        }
        
    },
    {
        "name": {
            "category_name_vector": [
                -0.2,
                -0.2,
                -0.2,
                ...
            ],
            "en": "this is 2nd name"
        }
    }
]
Filtered Process Map 
{
  "category":` `
        {
            "name": {
                "category_name_vector": ["this is 2nd name", "this is 2nd name" ]
            }
        }
}
Filtered Inference List
["this is 2nd name"]
Ingested Document
ID: 1
"category": [
    {
        "name": {
            "category_name_vector" [ // generated via model inference
                -0.2,
                -0.2,
                -0.2,
                ...
            ],
            "en":"this is 2nd name"**
        }
    },
    {
        "name": {
            "category_name_vector": [ // copied from existing document
                -0.2,
                -0.2,
                -0.2,
                ...
            ],
            "en": "this is 2nd name"
        }
    }
]

Explanation: The inference text at index 0 changed from “this is 1st name” to “this is 2nd name”, while the text at index 1 (“this is 2nd name”) remained unchanged. An inference call is made only for the modified text at index 0. Even if the existing document already contains embeddings for “this is 2nd name”, the inference call is still triggered because text changes are evaluated based on their respective indices, not on content duplication.

Scenario 3: Document Update with change to only irrelevant fields

Update Document
ID: 1
"category": [
    {
        "name": {
            "en": "this is 1st name"
        }
    },
    {
        "name": {
            "en": "this is 2nd name"
        }
    }
]
"irrelevant_field": "hello world"
Existing Document
ID: 1
"category": [
    {
        "name": {
            "category_name_vector": [
                -0.1,
                -0.1,
                -0.1,
                ...
            ],
            "en": "this is 1st name"
        }
        
    },
    {
        "name": {
            "category_name_vector": [
                -0.2,
                -0.2,
                -0.2,
                ...
            ],
            "en": "this is 2nd name"
        }
    }
]
Filtered Process Map 
{
  "category":` `
        {
            "name": {
                "category_name_vector": ["this is 1st name", "this is 2nd name"]
            }
        }
}
Filtered Inference List
[]
Ingested Document
"category": [
    {
        "name": {
            "category_name_vector": [ // copied from existing document
                -0.1,
                -0.1,
                -0.1,
                ...
            ],
            "en": "this is 1st name"
        }
        
    },
    {
        "name": {
            "category_name_vector": [ // copied from existing document
                -0.2,
                -0.2,
                -0.2,
                ...
            ],
            "en": "this is 2nd name"
        }
    }
]
**"irrelevant_field": "hello world"**

Explanation: “irrelevant_field” is not mapped with vector embedding, so inference call is not made regardless of the feature. Meanwhile, both inference texts “this is 1st name”, “this is 2nd name” mapped with embedding have remained unchanged, so the corresponding embedding values have been copied over instead of making inference calls.

Scenario 4: Document Update with changes in list values

Update Document
ID: 1
"category": ["category3", "category2"]
 
Existing Document
ID: 1
"category": ["category1", "category2"]
"category_knn": [
    {
        "knn": [
             -0.1,
             -0.1,
             -0.1,
        ]
    },
    {
        "knn": [
             -0.2,
             -0.2,
             -0.2,
        ]
    }
]
Filtered Process Map 
{
  "category_knn":` ["category3", "category2"]`
}
Filtered Inference List
["category3", "category2"]
Ingested Document
ID: 1
"category": ["category3", "category2"]
"category_knn": [
    {
        **"knn": [ // generated via inference call 
             -0.3,
             -0.3,
             -0.3,
        ]**
    },
    {
        "knn": [ // generated via inference call
             -0.2,
             -0.2,
             -0.2,
        ]
    }
]

Explanation: The optimized text embedding processor compares lists as a whole. If all texts in the list match, the existing embeddings are copied over. If any of the texts doesn't match, inference call is made for all values in the list regardless of matching texts

Testing:
Benchmark Test in progress. Will discuss the results before merge to main

Related Issues

Resolves #[Issue number to be closed when this PR is merged]

HLD: #1138

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • API changes companion pull request created.
  • Commits are signed per the DCO using --signoff.
  • Public documentation issue/PR created.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

@codecov
Copy link

codecov bot commented Feb 19, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 81.89%. Comparing base (628cb64) to head (8bc759e).
Report is 2 commits behind head on optimized-processor.

Additional details and impacted files
@@                    Coverage Diff                    @@
##             optimized-processor    #1191      +/-   ##
=========================================================
+ Coverage                  81.74%   81.89%   +0.14%     
+ Complexity                  2511     1301    -1210     
=========================================================
  Files                        190       97      -93     
  Lines                       8564     4429    -4135     
  Branches                    1436      749     -687     
=========================================================
- Hits                        7001     3627    -3374     
+ Misses                      1006      512     -494     
+ Partials                     557      290     -267     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

*/

@Log4j2
public abstract class OptimizedInferenceProcessor extends InferenceProcessor {
Copy link
Member

Choose a reason for hiding this comment

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

I feel the term "optimization" is too generic to use

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for pointing this out. Will wait to see if others have better opinions on the naming

Copy link
Member

Choose a reason for hiding this comment

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

I think I agree with @junqiu-lei here. There is a already a InferenceProcessor and we are trying to Optimize what i do already. So better if we name it around what optimization it does upon the existing one.
But if we are not able to come up with something more appropriate then I am good with this one. No strong concerns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will name as ConditionalTextEmbeddingProcessor to convey that it selectively generates embeddings when conditions are met. Let me know if there are better suggestions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will name it SelectiveTextEmbeddingProcessor for now


public static final String TYPE = "text_embedding";
public static final String LIST_TYPE_NESTED_MAP_KEY = "knn";
public static final String IGNORE_EXISTING = "ignore_existing";
Copy link
Member

Choose a reason for hiding this comment

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

From user's point, they might not clear what we're ignoring. maybe rename the flag to something like forceRegeneration or alwaysGenerateEmbedding?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ignore prefix was what we preferred based on discussions I had with ML-commons team because it is consistent with other flags that are in use. But open to more suggestions

Copy link
Contributor

Choose a reason for hiding this comment

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

I like "ignore_existing_embedding" since it's more descriptive. If the default to maintain backwards compatibility is to have optimization disabled, "ignore" makes it clearer that it's a feature they have to turn on to get the benefit (default false). like I wonder if users would be confused if a setting labelled "force" was default true?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just saw the above comment about the name of the processor too, maybe ideally the processor name and the option name are similar since the processor exists to support the option?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to make the naming more clear. ignore_existing is a little bit confusing that we actually want to do the optimization when it is true. People can view it as ignore_existing_embedding which means we want to always do the inference work.

I think even we can document how the parameter works it should be self explain and give people a gut feel what does it control. It should convey the info that we will do some optimization when the doc has existing embedding. It's actually kind weird we have to start with ignore. And if starting with ignore can make it confusing we should propose to not do that. I feel even we simply call it as "optimize_embedding_generation" is more descriptive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what do we think about skipExisting?

Copy link
Collaborator

Choose a reason for hiding this comment

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

That is a good point. How about skip_existing?

Copy link
Member

Choose a reason for hiding this comment

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

Do we have any integration test covered for the new processors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will include IT tests in this PR

@junqiu-lei
Copy link
Member

We might need BWC tests to verify mixed-version cluster scenarios where:

  • Some nodes are running the old version (without optimized processor)
  • Some nodes are running the new version

Copy link
Contributor

@q-andy q-andy left a comment

Choose a reason for hiding this comment

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

If I'm understanding correctly, when a user adds a text embedding processor to the pipeline with the config option as true, then we create a different processor object than if the config was false.

Are there any scenarios where this is confusing for users that the processor object we create isn't the exact same one that they define? Or is this all abstracted away?

Comment on lines 151 to 154
int min = Math.min(((List) sourceList.get()).size(), ((List) existingList.get()).size());
for (int j = 0; j < min; j++) {
if (((List) sourceList.get()).get(j).equals(((List) existingList.get()).get(j))) {
updatedEmbeddings.add(((List) embeddingList.get()).get(j));
Copy link
Contributor

Choose a reason for hiding this comment

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

Would having a helper method to cast lists would make this more readable? Like private List asList(Object object). Since raw casting requires double parentheses making it hard to parse inline, compare ((List) object) vs asList(object).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will make it more readable. thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need an extra method here. You just need to declare another variable to avoid repetitive casting.

List sList = (List)sourceList.get();
List eList = (List)existsingList.get();
if (sList.get(j).equals(eList.get(j)) {
 ...
}

@will-hwang
Copy link
Contributor Author

If I'm understanding correctly, when a user adds a text embedding processor to the pipeline with the config option as true, then we create a different processor object than if the config was false.

Are there any scenarios where this is confusing for users that the processor object we create isn't the exact same one that they define? Or is this all abstracted away?

the implementation detail should be abstracted away from user, and all user needs to know is the behavior of the flag.

Map<String, Object> existingSourceAndMetadataMap,
int index
) {
String textKey = ProcessorUtils.findKeyFromFromValue(ProcessorDocumentUtils.unflattenJson(fieldMap), currentPath, level);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I notice we do ProcessorDocumentUtils.unflattenJson(fieldMap) multiple times. Can we only do it once?

Copy link
Member

Choose a reason for hiding this comment

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

+1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed. unflattened in constructor

Copy link
Member

@vibrantvarun vibrantvarun left a comment

Choose a reason for hiding this comment

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

Completed the 1st round of review. Will wait for comments to be addressed before doing 2nd round of review.

Thanks

return v1;
} else {
return v2;
if (v1 instanceof List<?> && v2 instanceof List<?>) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you Add a comment on what does this peice of code do?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a leftover from previous code that lacks comments explaining its functionality. It would be great if you could add a comment to clarify it while making your modifications, helping to address this technical debt.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added comments

return v1List;
}

if (v1 instanceof Map<?, ?> && v2 instanceof Map<?, ?>) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you Add a comment on what does this peice of code do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, will add comments for this method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reverted back to previous after updated implementation

IngestDocument ingestDocument,
Map<String, Object> processorMap,
List<?> results,
boolean update
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
boolean update
boolean update

Can you come up with a little usecase specific variable name?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Or adding a javadoc explaining each parameter for this method might be good as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added javadoc for this

}
}

protected void setVectorFieldsToDocument(IngestDocument ingestDocument, Map<String, Object> processorMap, List<?> results) {
Copy link
Member

Choose a reason for hiding this comment

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

When is this method called? I see there are 2 methods with the same name. If we need this method then please change the name of this method to usecase specific.

Copy link
Member

Choose a reason for hiding this comment

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

Or better is to make the earlier method except this flag and put if else

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree. We can remove this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored to remove this method

// build nlp output for object in sourceValue which is map type
Iterator<Map<String, Object>> iterator = sourceAndMetadataMapValueInList.iterator();
IntStream.range(0, sourceAndMetadataMapValueInList.size()).forEach(index -> {
IndexWrapper listIndexWrapper = new IndexWrapper(0);
Copy link
Member

Choose a reason for hiding this comment

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

We don't needs this listIndexWrapper object. The argument indexWrapper is same object what you are trying to create here. Please traceback the indexWrapper object and you will find that it is called from buildNLPResult method in this class https://github.com/opensearch-project/neural-search/pull/1191/files#diff-d2b79f65d193c79dd65558833fcf583eb2c29301325e1eb4eb83a963c737f2f8R468. Which has the same declaration. They what is the need of making this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

indexWrapper is used to iterate the embedding list for update. listIndexWrapper is used to replace the existingIntStream.range(0, sourceAndMetadataMapValueInList.size()). Currently, every update is just another ingest so we overwrite the values and its embeddings every time. With the new way, some values and its embeddings will have been copied over, so we need a way to skip overwriting those values

Copy link
Member

Choose a reason for hiding this comment

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

If I am understanding correctly then we need only listIndexMapper not the indexMapper. As the listIndexMapper have the details of what needs to be updated right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this change was done to increment the nestedIndex based on the existence of embedding in document, instead of sequentially incrementing like IntStream.range(0, sourceAndMetadataMapValueInList.size()). This change was done to handle recently found edge cases such as #1024. Since this change is only needed only for specific cases and seems like a distraction to overall objective of the PR, I will raise a separate PR for this change.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The IndexWrapper is necessary. We want to increase counter only when we consumed value from inputNestedMapEntryValue so we need a way to keep the counter value. I see the problem is in the class name which is confusing because index in OpenSearch has its own meaning.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated implementation logic to remove the need for this change.

IndexWrapper indexWrapper,
Map<String, Object> sourceAndMetadataMap,
int nestedElementIndex
IndexWrapper listIndexWrapper
Copy link
Member

Choose a reason for hiding this comment

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

indexWrapper and listIndexWrapper are same. Please reevaluate the need of this object here.


public static final String MODEL_ID_FIELD = "model_id";
public static final String FIELD_MAP_FIELD = "field_map";
private static final BiFunction<Object, Object, Object> REMAPPING_FUNCTION = (v1, v2) -> {
Copy link
Member

Choose a reason for hiding this comment

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

Can we make this REMAPPING _Function a method? Loading this method in memory by default is not ideal.

Copy link
Collaborator

@heemin32 heemin32 Feb 21, 2025

Choose a reason for hiding this comment

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

Not sure if making it as method is necessary. In terms of loading into memory, there is no difference between this and method.

return new TextEmbeddingProcessor(tag, description, batchSize, modelId, filedMap, clientAccessor, environment, clusterService);
Map<String, Object> fieldMap = readMap(TYPE, tag, config, FIELD_MAP_FIELD);
boolean ignoreExisting = readBooleanProperty(TYPE, tag, config, IGNORE_EXISTING, DEFAULT_IGNORE_EXISTING);
if (ignoreExisting == true) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When we say ignore_existing does that means we want to always do inference work? Which is our existing behavior so we should use the existing processor rather than the optimized one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

open to suggestions, we can make the feature name more descriptive

) {
String index = ingestDocument.getSourceAndMetadata().get(INDEX_FIELD).toString();
String id = ingestDocument.getSourceAndMetadata().get(ID_FIELD).toString();
openSearchClient.execute(GetAction.INSTANCE, new GetRequest(index, id), ActionListener.wrap(response -> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In this processor we always do this get call which kind downgrade the performance of the operations we know for sure there is no existing embedding can be reused. We can control this through the ignore_existing flag but just thinking would it better in this optimized processor we also consider if the we are processing a create doc API? I think we can tell this info based on the which API is invoked and attach it to the IngestDocument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that would require model change in ingestDocument to attach API performed on the ingestDocument. This change should be a separate feature in my opinion, and should be implemented separately if needed

Copy link
Collaborator

Choose a reason for hiding this comment

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

There is no difference between create doc and update doc API.

return new TextEmbeddingProcessor(tag, description, batchSize, modelId, filedMap, clientAccessor, environment, clusterService);
Map<String, Object> fieldMap = readMap(TYPE, tag, config, FIELD_MAP_FIELD);
boolean ignoreExisting = readBooleanProperty(TYPE, tag, config, IGNORE_EXISTING, DEFAULT_IGNORE_EXISTING);
if (ignoreExisting == true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Since ignoreExisting is already a boolean you don't need to check equality to true here, you can use the boolean as the condition

Copy link
Member

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

It is only applicable for false conditions. If(ignoreExisting) by all means mean a true condition.

Copy link
Member

Choose a reason for hiding this comment

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

Therefore, it is recommended to use if(boolvariable) rather than if(boolvariable==true). The end goal is to improve readability. For false conditions it makes sense to write that explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay, will change

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is standard format for OpenSearch core where we explicitly compare it to either true or false. Even if we don't force it in neural, would be nice to follow it.

} else {
return v2;
if (v1 instanceof List<?> && v2 instanceof List<?>) {
List<Object> v1List = new ArrayList<>((List<?>) v1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we avoid creating new array here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored

List<?> v2List = (List<?>) v2;

Iterator<?> iterator = v2List.iterator();
for (int i = 0; i < v1List.size() && iterator.hasNext(); i++) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. In previous code, we added all item from v2 to v1. Now, we are setting null value in v1 using v2 value. Could you tell how this new logic can cover the previous use case as well?
  2. The assumption here is that, the number of null value in v1List is same as the size of v2. Can we make it obvious from the code? For example we can do like
for (int i = 0; i , v1List.size(); i++) {
  if (v1List.get(i) == null) {
    assert iterator.hasNext() == true;
    v1List.set(i, iterator.next());
  }
}
assert iterator.hasNext() == false;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored with comments

}

if (v1 instanceof Map<?, ?> && v2 instanceof Map<?, ?>) {
Map<String, Object> v1Map = new LinkedHashMap<>((Map<String, Object>) v1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we avoid creating new map? This might be costly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We checked if v1 is instance of Map<?, ?> but not we are casting it to Map<String, Object>. This is not safe.

}
return v1Map;
}
return v2 != null ? v2 : v1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we ever get to this case? What is the logic behind this returning v2 and ignoring v1?

Map<?, ?> v2Map = (Map<?, ?>) v2;

for (Map.Entry<?, ?> entry : v2Map.entrySet()) {
if (entry.getKey() instanceof String && !v1Map.containsKey(entry.getKey())) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a case where v2Map has different types of key? Like one key is string type and other int type?

Map<String, Object> currentMap = sourceAsMap;
String targetValue = keys[keys.length - 1];
for (String key : keys) {
if (key.equals(targetValue)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

what if path is "level1.level2.level1" and level is 3?
The code will break here and lastFoundKey will be null.

     * map:
     *  {
     *     "level1": {
     *          "level2" : {
     *              "first_text": "level1"
     *          }
     *      }
     * }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed, and added a test case

if (currentMap.containsKey(key)) {
Object value = currentMap.get(key);
if (value instanceof Map) {
currentMap = (Map<String, Object>) value;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unsafe cast

Copy link
Member

Choose a reason for hiding this comment

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

+1

public static final String TYPE = "text_embedding";
public static final String LIST_TYPE_NESTED_MAP_KEY = "knn";
public static final String SKIP_EXISTING = "skip_existing";
public static final boolean DEFAULT_SKIP_EXISTING = Boolean.FALSE;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public static final boolean DEFAULT_SKIP_EXISTING = Boolean.FALSE;
public static final boolean DEFAULT_SKIP_EXISTING = false;

You are using primitive datatype so false should be fine.

}
}
}, e -> { handler.accept(null, e); }));
} else { // skip existing flag is turned off. Call model inference without filtering
Copy link
Member

Choose a reason for hiding this comment

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

Add comment in new line.

return currentValue;
}

public static void setValueToSource(Map<String, Object> sourceAsMap, String targetKey, Object targetValue) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need this method? There is another method with the same name below. Can you combine both of them?

Copy link
Contributor Author

@will-hwang will-hwang Feb 27, 2025

Choose a reason for hiding this comment

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

i wanted to keep it consistent with getValueFromSource method, which can support two types of nested maps, one with just nested maps (link), and one with nested maps with lists (link)

}
}

public static Optional<Object> getValueFromSource(final Map<String, Object> sourceAsMap, final String targetField) {
Copy link
Member

Choose a reason for hiding this comment

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

There is a method with same name and return type below. Can you combine this and the below one? Why do we need this separate method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is an existing method that assumes nested structure will not contain lists.

// have been copied
String index = ingestDocument.getSourceAndMetadata().get(INDEX_FIELD).toString();
String id = ingestDocument.getSourceAndMetadata().get(ID_FIELD).toString();
openSearchClient.execute(GetAction.INSTANCE, new GetRequest(index, id), ActionListener.wrap(response -> {
Copy link
Collaborator

@heemin32 heemin32 Feb 27, 2025

Choose a reason for hiding this comment

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

I have a concern with this async operation. This might impact to other search request by consuming all search thread.
There are three options I can think of,

  1. we might need to have a circuit breaker to limit the total number of concurrent ingestion request.
  2. ask cx to pass embedding if they want to skip inference call so that we only can access the source document without existing document or cx can omit the text field so that inference can be skipped for update operation.
  3. Make it as synchronous.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

discussed offline, we'll first perform benchmark testing to see the impact on the current change

Map<String, Object> filteredProcessMap = new HashMap<>();
Map<String, Object> castedProcessMap = ProcessorUtils.castToMap(processMap);
for (Map.Entry<?, ?> entry : castedProcessMap.entrySet()) {
if ((entry.getKey() instanceof String) == false) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You don't need this check. castedProcessMap is already Map<String, Object> and the key is String.

}
Map<String, Object> filteredProcessMap = new HashMap<>();
Map<String, Object> castedProcessMap = ProcessorUtils.castToMap(processMap);
for (Map.Entry<?, ?> entry : castedProcessMap.entrySet()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
for (Map.Entry<?, ?> entry : castedProcessMap.entrySet()) {
for (Map.Entry<String, Object> entry : castedProcessMap.entrySet()) {

} else if (value instanceof List) {
List<Object> processedList = filterListValue(
currentPath,
(List<Object>) value,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not safe as well. If you know that value will be always List, let's use

@SuppressWarnings("unchecked")
    public static List<Object> castToObjectList(List<?> obj) {
        return (List<Object>) obj;
    }

// return empty list if processList and existingList are equal and embeddings are copied, return empty list otherwise
return filterInferenceValuesInList(
processList,
(List<Object>) existingList.get(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same for this. Not all List is List. For example, we cannot cast List to List

@will-hwang will-hwang force-pushed the optimized_text_embedding_processor_single_doc_update branch from 90e77d1 to 9bf5cce Compare February 28, 2025 00:16
@will-hwang will-hwang changed the base branch from main to optimized-processor February 28, 2025 00:17
@will-hwang will-hwang force-pushed the optimized_text_embedding_processor_single_doc_update branch 3 times, most recently from b1816f1 to 5e7f19e Compare February 28, 2025 00:43
}
}
}, e -> { handler.accept(null, e); }));
} else {
Copy link
Collaborator

@heemin32 heemin32 Feb 28, 2025

Choose a reason for hiding this comment

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

Nit. Better to handle simple case at top and reduce the nested depth?

if (skipExisting == false) {
  makeInferenceCall(ingestDocument, processMap, inferenceList, handler);
  return; 
}

// rest of codes..

// This method should be used only when you are certain the object is a `Map<String, Object>`.
// It is recommended to use this method as a last resort.
@SuppressWarnings("unchecked")
public static Map<String, Object> castToMap(Object obj) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public static Map<String, Object> castToMap(Object obj) {
public static Map<String, Object> unsafeCastToObjectMap(Object obj) {

// This method should be used only when you are certain the object is a `List<Object>`.
// It is recommended to use this method as a last resort.
@SuppressWarnings("unchecked")
public static List<Object> castToObjectList(Object obj) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public static List<Object> castToObjectList(Object obj) {
public static List<Object> unsafeCastToObjectList(Object obj) {

*
* @param sourceAsMap The Source map (a map of maps) to iterate through
* @param targetField The path to take to get the desired mapping
* @param index the index to use when a list is encountered during traversal; if list processing is not needed,
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the case when list processing is not needed when the value is list? In such case, returning empty optional is expected behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

user should always pass in index, if they know sourceAsMap contains list. In any other case, we should return empty optional to indicate value not found.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then the design of this method is not correct imo. If the caller already know that it contains list, they just get the value directly or there should be a separate method for that.

}
Map<String, Object> currentMap = (Map<String, Object>) value;
return Optional.ofNullable(currentMap.get(key));
return Optional.empty();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit. For me better to put this under else block to make sure every block should return itself without missing any condition.

else {
  Optional.empty();
}

return Optional.empty();
});

if (currentValue.isEmpty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe break; could be better?


for (int i = 0; i < keys.length - 1; i++) {
Object next = current.computeIfAbsent(keys[i], k -> new HashMap<>());
if (next instanceof ArrayList<?> list) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same, is there a case where next is array list but index is -1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if user assumes map doesn't contain lists, and doesn't specify the index, it will not set the value to map

*
* @param sourceAsMap The Source map (a map of maps) to iterate through
* @param targetField The path to take to get the desired mapping
* @param index the index to use when a list is encountered during traversal; if list processing is not needed,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Then the design of this method is not correct imo. If the caller already know that it contains list, they just get the value directly or there should be a separate method for that.

* Stores the reverse mapping of field names to support efficient lookups for embedding keys.
* This is generated by flattening and flipping the provided field map.
*/
protected Map<String, String> reversedFieldMap;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you comment what is the example key and value here?

existingSourceAndMetadataMap
);
filteredProcessMap.put(key, processedList);
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please check if value is String and throw exception otherwise.

if (embeddingList.isPresent() == false || embeddingList.get() instanceof List == false) {
return processList;
}
// return empty list if processList and existingList are equal and embeddings are copied, return empty list otherwise
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not correct

String newKey = prefix.isEmpty() ? key : prefix + "." + key;

if (value instanceof Map) {
flattenAndFlip(newKey, (Map<String, Object>) value, flippedMap);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not safe.

String transformedValue = parentPath.isEmpty() ? value.toString() : parentPath + "." + value.toString();
if (flippedMap.containsKey(transformedValue)) {
int index = 1;
while (flippedMap.containsKey(transformedValue + "_" + index)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not efficient..

private Map<String, Object> filter(
Map<String, Object> existingSourceAndMetadataMap,
Map<String, Object> sourceAndMetadataMap,
Object processMap,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Object processMap,
Map<String, Object> processMap,

Object processMap,
String traversedPath
) {
if (processMap instanceof Map == false) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (processMap instanceof Map == false) {

Object value = entry.getValue();
String currentPath = traversedPath.isEmpty() ? key : traversedPath + "." + key;
if (value instanceof Map<?, ?>) {
Map<String, Object> filteredInnerMap = filter(existingSourceAndMetadataMap, sourceAndMetadataMap, value, currentPath);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Map<String, Object> filteredInnerMap = filter(existingSourceAndMetadataMap, sourceAndMetadataMap, value, currentPath);
Map<String, Object> filteredInnerMap = filter(existingSourceAndMetadataMap, sourceAndMetadataMap, unsafeCastToObjectMap(value), currentPath);

* @return A possible result within an optional
*/
public static Optional<Object> getValueFromSource(final Map<String, Object> sourceAsMap, final String targetField) {
public static Optional<Object> getValueFromSource(final Map<String, Object> sourceAsMap, final String targetField, int index) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public static Optional<Object> getValueFromSource(final Map<String, Object> sourceAsMap, final String targetField, int index) {
public static List<Optional<Object>> getValueFromSource(final Map<String, Object> sourceAsMap, final String targetField, int index) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

left it as <Optional<Object>> that can return single value, list of values and list of list values. If there's concern, please let me know

@will-hwang will-hwang force-pushed the optimized_text_embedding_processor_single_doc_update branch 2 times, most recently from 1daf07d to 8902065 Compare March 4, 2025 21:11
String key = entry.getKey();
Object value = entry.getValue();
String currentKey = path.isEmpty() ? key : path + "." + key;
if (Objects.isNull(currentKey)) return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't we check key but not currentKey?

Suggested change
if (Objects.isNull(currentKey)) return;
if (Objects.isNull(key)) return;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this check is not needed in valid cases, added to avoid unit test failure

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you fix the unit text?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is the failing unit test case, which checks for failure case: https://github.com/opensearch-project/neural-search/blob/main/src/test/java/org/opensearch/neuralsearch/processor/TextEmbeddingProcessorTests.java#L122-L135

I can modify the implementation to bypass this unit test case

if (value instanceof Map) {
flattenAndFlip(currentKey, ProcessorUtils.unsafeCastToObjectMap(value), flippedMap);
} else if (value instanceof String) {
String traversedPath = currentKey.contains(".") ? currentKey.substring(0, currentKey.lastIndexOf('.')) : "";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
String traversedPath = currentKey.contains(".") ? currentKey.substring(0, currentKey.lastIndexOf('.')) : "";
String traversedPath = path;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

path may not contain fully traversed path when we get . separated path.
we need to keep track of traversed path with currentKey value.

As an example, for given map:
{level1.level2: knn_field}

we need to insert into flipped map the full path to value: level1.level2.knn_field, instead of just knn_field in this case

@will-hwang will-hwang force-pushed the optimized_text_embedding_processor_single_doc_update branch from 8902065 to 8bc759e Compare March 4, 2025 23:19
Copy link
Collaborator

@heemin32 heemin32 left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks.

@heemin32 heemin32 merged commit 1a6e58e into opensearch-project:optimized-processor Mar 5, 2025
49 of 51 checks passed
@vibrantvarun
Copy link
Member

@heemin32 I had comments added earlier in this PR. Not sure, why we closed this PR early? I will add my comments once this PR raised for main.

Thanks

@heemin32
Copy link
Collaborator

heemin32 commented Mar 5, 2025

@vibrantvarun Closed it as I thought all the comments are resolved. Could you point out comments which are not resolved or add one if there is something new? @will-hwang can address them in a new PR. Adding comment when this is merged to main will be too late.

will-hwang added a commit to will-hwang/neural-search that referenced this pull request Mar 20, 2025
will-hwang added a commit to will-hwang/neural-search that referenced this pull request Mar 20, 2025
heemin32 pushed a commit that referenced this pull request Mar 25, 2025
…1238)

* implement single document update scenario for text embedding processor (#1191)

Signed-off-by: Will Hwang <[email protected]>

* implement batch document update scenario for text embedding processor (#1217)

Signed-off-by: Will Hwang <[email protected]>

---------

Signed-off-by: Will Hwang <[email protected]>
ryanbogan pushed a commit to ryanbogan/neural-search that referenced this pull request Apr 10, 2025
…pensearch-project#1238)

* implement single document update scenario for text embedding processor (opensearch-project#1191)

Signed-off-by: Will Hwang <[email protected]>

* implement batch document update scenario for text embedding processor (opensearch-project#1217)

Signed-off-by: Will Hwang <[email protected]>

---------

Signed-off-by: Will Hwang <[email protected]>
YeonghyeonKO pushed a commit to YeonghyeonKO/neural-search that referenced this pull request May 30, 2025
…pensearch-project#1238)

* implement single document update scenario for text embedding processor (opensearch-project#1191)

Signed-off-by: Will Hwang <[email protected]>

* implement batch document update scenario for text embedding processor (opensearch-project#1217)

Signed-off-by: Will Hwang <[email protected]>

---------

Signed-off-by: Will Hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>
YeonghyeonKO pushed a commit to YeonghyeonKO/neural-search that referenced this pull request May 30, 2025
…pensearch-project#1238)

* implement single document update scenario for text embedding processor (opensearch-project#1191)

Signed-off-by: Will Hwang <[email protected]>

* implement batch document update scenario for text embedding processor (opensearch-project#1217)

Signed-off-by: Will Hwang <[email protected]>

---------

Signed-off-by: Will Hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>
yuye-aws pushed a commit that referenced this pull request Jun 10, 2025
#1342)

* Implement Optimized embedding generation in text embedding processor (#1238)

* implement single document update scenario for text embedding processor (#1191)

Signed-off-by: Will Hwang <[email protected]>

* implement batch document update scenario for text embedding processor (#1217)

Signed-off-by: Will Hwang <[email protected]>

---------

Signed-off-by: Will Hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Going from alpha1 to beta1 for 3.0 release (#1245)

Signed-off-by: yeonghyeonKo <[email protected]>

* Implement Optimized embedding generation in sparse encoding processor (#1246)

Signed-off-by: Will Hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Implement Optimized embedding generation in text and image embedding processor (#1249)

Signed-off-by: will-hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Inner hits support with hybrid query (#1253)

* Inner Hits in Hybrid query

Signed-off-by: Varun Jain <[email protected]>

* Inner hits support with hybrid query

Signed-off-by: Varun Jain <[email protected]>

* Add changelog

Signed-off-by: Varun Jain <[email protected]>

* fix integ tests

Signed-off-by: Varun Jain <[email protected]>

* Modify comment

Signed-off-by: Varun Jain <[email protected]>

* Explain test case

Signed-off-by: Varun Jain <[email protected]>

* Optimize inner hits count calculation method

Signed-off-by: Varun Jain <[email protected]>

---------

Signed-off-by: Varun Jain <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Support custom tags in semantic highlighter (#1254)

Signed-off-by: yeonghyeonKo <[email protected]>

* Add neural stats API (#1256)

* Add neural stats API

Signed-off-by: Andy Qin <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Added release notes for 3.0 beta1 (#1252)

* Added release notes for 3.0 beta1

Signed-off-by: Martin Gaievski <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Update semantic highlighter test model (#1259)

Signed-off-by: Junqiu Lei <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Fix the edge case when the value of a fieldMap key in ingestDocument is empty string (#1257)

Signed-off-by: Chloe Gao <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Hybrid query should call rewrite before creating weight (#1268)

* Hybrid query should call rewrite before creating weight

Signed-off-by: Harsha Vamsi Kalluri <[email protected]>

* Awaits fix

Signed-off-by: Harsha Vamsi Kalluri <[email protected]>

* Rewrite with searcher

Signed-off-by: Harsha Vamsi Kalluri <[email protected]>

* Feature flag issue

Signed-off-by: Harsha Vamsi Kalluri <[email protected]>

---------

Signed-off-by: Harsha Vamsi Kalluri <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Support phasing off SecurityManager usage in favor of Java Agent (#1265)

Signed-off-by: Gulshan <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Fix multi node transport issue on NeuralKNNQueryBuilder originalQueryText (#1272)

Signed-off-by: Junqiu Lei <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add semantic field mapper. (#1225)

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Increment version to 3.0.0-SNAPSHOT (#1286)

Signed-off-by: opensearch-ci-bot <[email protected]>
Co-authored-by: opensearch-ci-bot <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Remove beta1 qualifier (#1292)

Signed-off-by: Peter Zhu <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Fix for merging scoreDocs when totalHits are greater than 1 and fieldDocs are 0 (#1295) (#1296)

(cherry picked from commit 6f3aabb)

Co-authored-by: Varun Jain <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* add release notes for 3.0 (#1287)

Signed-off-by: will-hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Allow maven to publish to all versions (#1300) (#1301)

Signed-off-by: Peter Zhu <[email protected]>
(cherry picked from commit c5625db)

Co-authored-by: Peter Zhu <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [FEAT] introduce new FixedStringLengthChunker

Signed-off-by: yeonghyeonKo <[email protected]>

* [TEST] initial test cases for FixedStringLengthChunker

Signed-off-by: yeonghyeonKo <[email protected]>

* [FIX] gradlew spotlessApply

Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] remove unnecessary comments

Signed-off-by: yeonghyeonKo <[email protected]>

* [Performance Improvement] Add custom bulk scorer for hybrid query (2-3x faster) (#1289)

Signed-off-by: Martin Gaievski <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add TextChunkingProcessor stats (#1308)

* Add TextChunkingProcessor stats

Signed-off-by: Andy Qin <[email protected]>

# Conflicts:
#	CHANGELOG.md

* Update unit and integ tests

Signed-off-by: Andy Qin <[email protected]>

---------

Signed-off-by: Andy Qin <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Update Lucene dependencies (#1336)

* Update Lucene dependencies

Signed-off-by: Ryan Bogan <[email protected]>

* Add changelog entry

Signed-off-by: Ryan Bogan <[email protected]>

* Update model request body for bwc and integ tests

Signed-off-by: Ryan Bogan <[email protected]>

---------

Signed-off-by: Ryan Bogan <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] modify algorithm name and related parts

Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] update test codes along with the change in CharacterLengthChunker

Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] remove defensive check to prevent adding redundant code lines

Signed-off-by: yeonghyeonKo <[email protected]>

* Update CharacterLengthChunker to FixedCharLengthChunker

Signed-off-by: Marcel Yeonghyeon Ko <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Update ChunkerFactory

Signed-off-by: Marcel Yeonghyeon Ko <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Update CharacterLengthChunkerTests to FixedCharLengthChunkerTests

Signed-off-by: Marcel Yeonghyeon Ko <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [FIX] handle a corner case where the content is shorter than charLimit

Signed-off-by: yeonghyeonKo <[email protected]>

* [TEST] Add integration test codes for fixed_char_length chunking algorithm

Signed-off-by: yeonghyeonKo <[email protected]>

* [TEST] integration test code for cascaded pipeline

Signed-off-by: yeonghyeonKo <[email protected]>

* Support analyzer-based neural sparse query (#1088)

* merge main; add analyzer impl

Signed-off-by: zhichao-aws <[email protected]>

* two phase adaption

Signed-off-by: zhichao-aws <[email protected]>

* two phase adaption

Signed-off-by: zhichao-aws <[email protected]>

* remove analysis

Signed-off-by: zhichao-aws <[email protected]>

* lint

Signed-off-by: zhichao-aws <[email protected]>

* update

Signed-off-by: zhichao-aws <[email protected]>

* address comments

Signed-off-by: zhichao-aws <[email protected]>

* tests

Signed-off-by: zhichao-aws <[email protected]>

* modify plugin security policy

Signed-off-by: zhichao-aws <[email protected]>

* change log

Signed-off-by: zhichao-aws <[email protected]>

* address comments

Signed-off-by: zhichao-aws <[email protected]>

* modify to package-private

Signed-off-by: zhichao-aws <[email protected]>

---------

Signed-off-by: zhichao-aws <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Fixed score value as null for single shard for sorting (#1277)

* Fixed score value as null for single shard for sorting

Signed-off-by: Owais <[email protected]>

* Addressed comment

Signed-off-by: Owais <[email protected]>

* Addressed more comments

Signed-off-by: Owais <[email protected]>

* Added UT

Signed-off-by: Owais <[email protected]>

---------

Signed-off-by: Owais <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add IT for neural sparse query + bert-uncased mbert-uncased analyzer (#1279)

* add it

Signed-off-by: zhichao-aws <[email protected]>

* change log

Signed-off-by: zhichao-aws <[email protected]>

---------

Signed-off-by: zhichao-aws <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add WithFieldName implementation to QueryBuilders (#1285)

Signed-off-by: Owais <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [AUTO] Increment version to 3.1.0-SNAPSHOT (#1288)

* Increment version to 3.1.0-SNAPSHOT

Signed-off-by: opensearch-ci-bot <[email protected]>

* Update build.gradle

Signed-off-by: Peter Zhu <[email protected]>

---------

Signed-off-by: opensearch-ci-bot <[email protected]>
Signed-off-by: Peter Zhu <[email protected]>
Co-authored-by: opensearch-ci-bot <[email protected]>
Co-authored-by: Peter Zhu <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* add release notes for 3.0 (#1298)

Signed-off-by: will-hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Return bad request for invalid stat parameters in stats API (#1291)

Signed-off-by: Andy Qin <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add semantic mapping transformer. (#1276)

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add semantic ingest processor. (#1309)

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [Performance Improvement] Add custom bulk scorer for hybrid query (2-3x faster) (#1289)

Signed-off-by: Martin Gaievski <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Implement the query logic for the semantic field. (#1315)

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Support custom weights params in RRF (#1322)

* Support Weights params in RRF

Signed-off-by: Varun Jain <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* add validation for invalid nested hybrid query (#1305)

* add validation for nested hybrid query

Signed-off-by: will-hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add stats tracking for semantic highlighting (#1327)

* Add stats tracking for semantic highlighting

Signed-off-by: Junqiu Lei <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Update Lucene dependencies (#1336)

* Update Lucene dependencies

Signed-off-by: Ryan Bogan <[email protected]>

* Add changelog entry

Signed-off-by: Ryan Bogan <[email protected]>

* Update model request body for bwc and integ tests

Signed-off-by: Ryan Bogan <[email protected]>

---------

Signed-off-by: Ryan Bogan <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Enhance semantic field to allow to enable/disable chunking. (#1337)

* Implement the query logic for the semantic field.

Signed-off-by: Bo Zhang <[email protected]>

* Enhance semantic field to allow to enable/disable chunking.

Signed-off-by: Bo Zhang <[email protected]>

---------

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] modify algorithm name and related parts

Signed-off-by: yeonghyeonKo <[email protected]>

* Update CHANGELOG.md

Signed-off-by: Marcel Yeonghyeon Ko <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* [FEAT] Add fixed_char_length chunking algorithm to STAT manager

Signed-off-by: yeonghyeonKo <[email protected]>

* [TEST] Add integration test codes for fixed_char_length chunking algorithm

Signed-off-by: yeonghyeonKo <[email protected]>

* [TEST] integration test code for cascaded pipeline

Signed-off-by: yeonghyeonKo <[email protected]>

* Going from alpha1 to beta1 for 3.0 release (#1245)

Signed-off-by: yeonghyeonKo <[email protected]>

* Fix multi node transport issue on NeuralKNNQueryBuilder originalQueryText (#1272)

Signed-off-by: Junqiu Lei <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add semantic field mapper. (#1225)

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Add semantic mapping transformer. (#1276)

Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>

* Fix multi node transport issue on NeuralKNNQueryBuilder originalQueryText (#1272)

Signed-off-by: Junqiu Lei <[email protected]>

* Add semantic field mapper. (#1225)

Signed-off-by: Bo Zhang <[email protected]>

* Add semantic mapping transformer. (#1276)

Signed-off-by: Bo Zhang <[email protected]>

* [FIX] minor typo

Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] adopt FixedTokenLengthChunker's loop strategy for robust final chunking

Signed-off-by: yeonghyeonKo <[email protected]>

* [TEST] sum the number of processors and their executions correctly in TextChunkingProcessorIT

Signed-off-by: yeonghyeonKo <[email protected]>

* [REFACTOR] gradlew spotlessApply

Signed-off-by: yeonghyeonKo <[email protected]>

---------

Signed-off-by: Will Hwang <[email protected]>
Signed-off-by: yeonghyeonKo <[email protected]>
Signed-off-by: will-hwang <[email protected]>
Signed-off-by: Varun Jain <[email protected]>
Signed-off-by: Andy Qin <[email protected]>
Signed-off-by: Martin Gaievski <[email protected]>
Signed-off-by: Junqiu Lei <[email protected]>
Signed-off-by: Chloe Gao <[email protected]>
Signed-off-by: Harsha Vamsi Kalluri <[email protected]>
Signed-off-by: Gulshan <[email protected]>
Signed-off-by: Bo Zhang <[email protected]>
Signed-off-by: opensearch-ci-bot <[email protected]>
Signed-off-by: Peter Zhu <[email protected]>
Signed-off-by: Ryan Bogan <[email protected]>
Signed-off-by: Marcel Yeonghyeon Ko <[email protected]>
Signed-off-by: zhichao-aws <[email protected]>
Signed-off-by: Owais <[email protected]>
Co-authored-by: Will Hwang <[email protected]>
Co-authored-by: Martin Gaievski <[email protected]>
Co-authored-by: Varun Jain <[email protected]>
Co-authored-by: Junqiu Lei <[email protected]>
Co-authored-by: Andy <[email protected]>
Co-authored-by: Chloe Gao <[email protected]>
Co-authored-by: Harsha Vamsi Kalluri <[email protected]>
Co-authored-by: Gulshan <[email protected]>
Co-authored-by: Bo Zhang <[email protected]>
Co-authored-by: opensearch-trigger-bot[bot] <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com>
Co-authored-by: opensearch-ci-bot <[email protected]>
Co-authored-by: Peter Zhu <[email protected]>
Co-authored-by: Ryan Bogan <[email protected]>
Co-authored-by: zhichao-aws <[email protected]>
Co-authored-by: Owais Kazi <[email protected]>
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.

6 participants