Skip to content

Commit

Permalink
add search helper
Browse files Browse the repository at this point in the history
  • Loading branch information
marevol committed Jun 22, 2024
1 parent 849027b commit 6b1d6ac
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 47 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,43 @@ See [Maven Repository](https://repo1.maven.org/maven2/org/codelibs/fess/fess-web

See [Plugin](https://fess.codelibs.org/14.15/admin/plugin-guide.html) of Administration guide.

## Usage

After installing the plugin, follow these steps to configure and use it:

1. **Start Fess**: Launch Fess and log in to the administration console.

2. **Configure System Properties**: Add the following properties to the general settings under system properties:
```
fess.multimodal.content.field=content_vector
fess.multimodal.content.dimension=512
fess.multimodal.content.method=hnsw
fess.multimodal.content.engine=lucene
fess.multimodal.content.space_type=cosinesimil
fess.multimodal.min_score=0.5
```

3. **Update Settings**: Navigate to the scheduler page and execu te Config Reloader.

4. **Re-indexing**: Navigate to the maintenance page and execute re-indexing.

5. **Setup CLIP as Service**: For image embedding, use CLIP as Service. Start the CLIP API using Docker:
```sh
git clone https://github.com/codelibs/fess-webapp-multimodal.git
cd fess-webapp-multimodal/docker
docker compose up -d
```
This will make the CLIP API accessible at `localhost:51000`.

6. **Crawl Directories**: Crawl directories containing image files.

7. **Test Data**: If you need test data, you can download the Open Images Dataset. For example:
```sh
pip install fiftyone
fiftyone zoo datasets load open-images-v7 --split validation --kwargs max_samples=1000 -d fiftyone
```
This will download 1000 images into the `fiftyone` directory.

## Contributing

We welcome contributions to enhance the functionality of this plugin. Please fork the repository and submit pull requests.
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>
org.codelibs.fess.crawler.multimodal</Automatic-Module-Name>
<Automatic-Module-Name>org.codelibs.fess.multimodal</Automatic-Module-Name>
<Fess-WebAppJar>true</Fess-WebAppJar>
</manifestEntries>
</archive>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,28 @@ public class MultiModalConstants {

private static final String PREFIX = "fess.multimodal.";

public static final String CONTENT_DIMENSION = PREFIX + "content.dimension";

public static final String CONTENT_ENGINE = PREFIX + "content.engine";

public static final String CONTENT_METHOD = PREFIX + "content.method";

public static final String CONTENT_SPACE_TYPE = PREFIX + "content.space_type";

public static final String CONTENT_FIELD = PREFIX + "content.field";

public static final String MIN_SCORE = PREFIX + "min_score";

public static final String CONTENT_VECTOR_FIELD = System.getProperty(PREFIX + "content.field", "content_vector");
public static final String DEFAULT_CONTENT_FIELD = PREFIX + "content_vector";

public static final String X_FESS_EMBEDDING = "X-FESS-Embedding";

public static final String SEARCHER = "multiModalSearcher";

public static final String CAS_CLIENT = "casClient";

public static final String HELPER = "multiModalSearchHelper";

private MultiModalConstants() {
// nothing
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ public class CasClient {
public void init() {
imageWidth = Integer.getInteger("clip.image.width", 224);
imageHeight = Integer.getInteger("clip.image.height", 224);
maxImageWidth = Integer.getInteger("clip.image.max_width", 1000);
maxImageHeight = Integer.getInteger("clip.image.max_height", 1000);
maxImageWidth = Integer.getInteger("clip.image.max_width", 3000);
maxImageHeight = Integer.getInteger("clip.image.max_height", 2000);
imageFormat = System.getProperty("clip.image.format", "png");
clipEndpoint = System.getProperty("clip.server.endpoint", "http://localhost:51000");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2012-2024 CodeLibs Project and the Others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.codelibs.fess.multimodal.helper;

import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_DIMENSION;
import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_ENGINE;
import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_FIELD;
import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_METHOD;
import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_SPACE_TYPE;
import static org.codelibs.fess.multimodal.MultiModalConstants.DEFAULT_CONTENT_FIELD;
import static org.codelibs.fess.multimodal.MultiModalConstants.MIN_SCORE;

import javax.annotation.PostConstruct;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codelibs.core.lang.StringUtil;
import org.codelibs.fess.es.client.SearchEngineClient;
import org.codelibs.fess.query.parser.QueryParser;
import org.codelibs.fess.util.ComponentUtil;

import com.google.common.base.CharMatcher;

public class MultiModalSearchHelper {
private static final Logger logger = LogManager.getLogger(MultiModalSearchHelper.class);

protected Float minScore;

private String vectorField;

@PostConstruct
public void init() {
final SearchEngineClient client = ComponentUtil.getSearchEngineClient();
client.addDocumentSettingRewriteRule(s -> s.replace("\"codec\":", "\"knn\": true,\"codec\":"));
client.addDocumentMappingRewriteRule(s -> {
final String dimension = System.getProperty(CONTENT_DIMENSION); // ex. 512
final String method = System.getProperty(CONTENT_METHOD); // ex. hnsw
final String engine = System.getProperty(CONTENT_ENGINE); // ex. lucene
final String spaceType = System.getProperty(CONTENT_SPACE_TYPE, "l2"); // ex. l2
if (logger.isDebugEnabled()) {
logger.debug("field: {}, dimension: {}, method: {}, engine: {}, spaceType: {}", vectorField, dimension, method, engine,
spaceType);
}
if (StringUtil.isBlank(dimension) || StringUtil.isBlank(vectorField) || StringUtil.isBlank(method)
|| StringUtil.isBlank(engine)) {
return s;
}
return s.replace("\"content\":", "\"" + vectorField + "\": {\n" //
+ " \"type\": \"knn_vector\",\n" //
+ " \"dimension\": " + dimension + ",\n" //
+ " \"method\": {\n" //
+ " \"name\": \"" + method + "\",\n" //
+ " \"engine\": \"" + engine + "\",\n" //
+ " \"space_type\": \"" + spaceType + "\"\n" //
+ " }\n" //
+ "},\n" //
+ "\"content\":");
});

if (ComponentUtil.hasQueryParser()) {
final QueryParser queryParser = ComponentUtil.getQueryParser();
queryParser.addFilter((query, chain) -> chain.parse(rewriteQuery(query)));
}

load();
ComponentUtil.getSystemHelper().addUpdateConfigListener("MultiModalSearch", this::load);
}

protected String load() {
final StringBuilder buf = new StringBuilder();

buf.append("vector_field=");
vectorField = System.getProperty(CONTENT_FIELD, DEFAULT_CONTENT_FIELD).trim(); // ex. content_vector
buf.append(vectorField);

buf.append(", min_score=");
final String minScoreValue = System.getProperty(MIN_SCORE);
if (StringUtil.isNotBlank(minScoreValue)) {
try {
minScore = Float.valueOf(minScoreValue);
buf.append(minScore);
} catch (final NumberFormatException e) {
logger.debug("Failed to parse {}.", minScoreValue, e);
minScore = null;
}
} else {
minScore = null;
}

return buf.toString();
}

protected String rewriteQuery(final String query) {
if (StringUtil.isBlank(query) || (query.indexOf('"') != -1) || !CharMatcher.whitespace().matchesAnyOf(query)) {
return query;
}

for (final String field : ComponentUtil.getQueryFieldConfig().getSearchFields()) {
if (query.indexOf(field + ":") != -1) {
return query;
}
}

return "\"" + query + "\"";
}

public Float getMinScore() {
return minScore;
}

public String getVectorField() {
return vectorField;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import static org.codelibs.core.lang.StringUtil.EMPTY;
import static org.codelibs.fess.Constants.MAPPING_TYPE_ARRAY;
import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_VECTOR_FIELD;
import static org.codelibs.fess.multimodal.MultiModalConstants.X_FESS_EMBEDDING;

import java.util.Map;
Expand All @@ -27,28 +26,36 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codelibs.fess.ingest.Ingester;
import org.codelibs.fess.multimodal.MultiModalConstants;
import org.codelibs.fess.multimodal.helper.MultiModalSearchHelper;
import org.codelibs.fess.multimodal.util.EmbeddingUtil;
import org.codelibs.fess.util.ComponentUtil;

public class EmbeddingIngester extends Ingester {
private static final Logger logger = LogManager.getLogger(EmbeddingIngester.class);

protected String vectorField;

@PostConstruct
public void init() {

ComponentUtil.getFessConfig().addCrawlerMetadataNameMapping(X_FESS_EMBEDDING, CONTENT_VECTOR_FIELD, MAPPING_TYPE_ARRAY, EMPTY);
final MultiModalSearchHelper helper = ComponentUtil.getComponent(MultiModalConstants.HELPER);
vectorField = helper.getVectorField();
ComponentUtil.getFessConfig().addCrawlerMetadataNameMapping(X_FESS_EMBEDDING, vectorField, MAPPING_TYPE_ARRAY, EMPTY);
if (logger.isDebugEnabled()) {
logger.debug("vector field: {}", vectorField);
}
}

@Override
protected Map<String, Object> process(final Map<String, Object> target) {
if (target.containsKey(CONTENT_VECTOR_FIELD)) {
logger.debug("[{}] : {}", CONTENT_VECTOR_FIELD, target);
if (target.get(CONTENT_VECTOR_FIELD) instanceof final String[] encodedEmbeddings) {
if (target.containsKey(vectorField)) {
logger.debug("[{}] : {}", vectorField, target);
if (target.get(vectorField) instanceof final String[] encodedEmbeddings) {
final float[] embedding = EmbeddingUtil.decodeFloatArray(encodedEmbeddings[0]);
logger.debug("embedding:{}", embedding);
target.put(CONTENT_VECTOR_FIELD, embedding);
target.put(vectorField, embedding);
} else {
logger.warn("{} is not an array.", CONTENT_VECTOR_FIELD);
logger.warn("{} is not an array.", vectorField);
}
}
return target;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.logging.log4j.Logger;
import org.apache.lucene.search.PhraseQuery;
import org.codelibs.fess.entity.QueryContext;
import org.codelibs.fess.entity.SearchRequestParams;
import org.codelibs.fess.multimodal.rank.fusion.MultiModalSearcher;
import org.codelibs.fess.multimodal.rank.fusion.MultiModalSearcher.SearchContext;
import org.codelibs.fess.mylasta.direction.FessConfig;
Expand All @@ -43,8 +44,9 @@ protected QueryBuilder convertPhraseQuery(final FessConfig fessConfig, final Que
}

final String text = String.join(" ", texts);
final QueryBuilder queryBuilder =
new MultiModalQueryBuilder.Builder().query(text).minScore(searchContext.getParams().getMinScore()).build().toQueryBuilder();
final SearchRequestParams params = searchContext.getParams();
final QueryBuilder queryBuilder = new MultiModalQueryBuilder.Builder().field(searchContext.getVectorField()).query(text)
.k(params.getPageSize()).build().toQueryBuilder();
context.addFieldLog(field, text);
context.addHighlightedQuery(text);
if (logger.isDebugEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package org.codelibs.fess.multimodal.query;

import static org.codelibs.fess.multimodal.MultiModalConstants.CAS_CLIENT;
import static org.codelibs.fess.multimodal.MultiModalConstants.CONTENT_VECTOR_FIELD;

import org.codelibs.fess.multimodal.client.CasClient;
import org.codelibs.fess.multimodal.index.query.KNNQueryBuilder;
Expand All @@ -25,7 +24,9 @@

public class MultiModalQueryBuilder {

protected String field;
protected String query;
protected int k;
protected Float minScore;

private MultiModalQueryBuilder() {
Expand All @@ -34,22 +35,36 @@ private MultiModalQueryBuilder() {

public static class Builder {

private String field;
private String query;
private int k = 10;
private Float minScore;

public Builder field(final String field) {
this.field = field;
return this;
}

public Builder query(final String query) {
this.query = query;
return this;
}

public Builder k(final int k) {
this.k = k;
return this;
}

public Builder minScore(final Float minScore) {
this.minScore = minScore;
return this;
}

public MultiModalQueryBuilder build() {
final MultiModalQueryBuilder builder = new MultiModalQueryBuilder();
builder.field = field;
builder.query = query;
builder.k = k;
builder.minScore = minScore;
return builder;
}
Expand All @@ -58,7 +73,7 @@ public MultiModalQueryBuilder build() {
public QueryBuilder toQueryBuilder() {
final CasClient client = ComponentUtil.getComponent(CAS_CLIENT);
final float[] embedding = client.getTextEmbedding(query);
return new KNNQueryBuilder.Builder().field(CONTENT_VECTOR_FIELD).vector(embedding).minScore(minScore).build();
return new KNNQueryBuilder.Builder().field(field).vector(embedding).minScore(minScore).k(k).build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.logging.log4j.Logger;
import org.apache.lucene.search.TermQuery;
import org.codelibs.fess.entity.QueryContext;
import org.codelibs.fess.entity.SearchRequestParams;
import org.codelibs.fess.multimodal.rank.fusion.MultiModalSearcher;
import org.codelibs.fess.multimodal.rank.fusion.MultiModalSearcher.SearchContext;
import org.codelibs.fess.mylasta.direction.FessConfig;
Expand All @@ -42,8 +43,9 @@ protected QueryBuilder convertDefaultTermQuery(final FessConfig fessConfig, fina
return super.convertDefaultTermQuery(fessConfig, context, termQuery, boost, field, text);
}

final QueryBuilder queryBuilder =
new MultiModalQueryBuilder.Builder().query(text).minScore(searchContext.getParams().getMinScore()).build().toQueryBuilder();
final SearchRequestParams params = searchContext.getParams();
final QueryBuilder queryBuilder = new MultiModalQueryBuilder.Builder().field(searchContext.getVectorField()).query(text)
.k(params.getPageSize()).build().toQueryBuilder();
context.addFieldLog(field, text);
context.addHighlightedQuery(text);
if (logger.isDebugEnabled()) {
Expand Down
Loading

0 comments on commit 6b1d6ac

Please sign in to comment.