Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/org/codelibs/fess/api/chat/ChatApiManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codelibs.core.lang.StringUtil;
import org.codelibs.fess.Constants;
import org.codelibs.fess.api.BaseApiManager;
import org.codelibs.fess.chat.ChatClient.ChatResult;
import org.codelibs.fess.chat.ChatPhaseCallback;
Expand Down Expand Up @@ -183,6 +184,11 @@ protected void processChatRequest(final HttpServletRequest request, final HttpSe
}

final String userId = getUserId(request);

// Set LLM type name as Access Type for search log
request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE,
ComponentUtil.getFessConfig().getSystemProperty("rag.llm.name", "ollama"));

final Map<String, String[]> fields = parseFieldFilters(request);
final String[] extraQueries = parseExtraQueries(request);
final ChatResult result;
Expand Down Expand Up @@ -257,6 +263,10 @@ protected void processStreamRequest(final HttpServletRequest request, final Http
try (final PrintWriter writer = response.getWriter()) {
final String userId = getUserId(request);

// Set LLM type name as Access Type for search log
request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE,
ComponentUtil.getFessConfig().getSystemProperty("rag.llm.name", "ollama"));

// Create phase callback for SSE events
final ChatPhaseCallback phaseCallback = new ChatPhaseCallback() {
@Override
Expand Down
143 changes: 105 additions & 38 deletions src/main/java/org/codelibs/fess/helper/SearchLogHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
import org.dbflute.optional.OptionalThing;
import org.lastaflute.web.util.LaRequestUtil;
import org.opensearch.action.update.UpdateRequest;

import jakarta.servlet.http.HttpServletRequest;
import org.opensearch.script.Script;

import com.fasterxml.jackson.core.JsonProcessingException;
Expand Down Expand Up @@ -124,6 +126,28 @@ public UserInfo load(final String key) throws Exception {
searchLogLogger = LogManager.getLogger(loggerName);
}

/** Holds resolved dependencies for search log creation, decoupled from ComponentUtil. */
protected static class SearchLogContext {
final FessConfig fessConfig;
final String[] roles;
final String userCode;
final String userId;
final HttpServletRequest request;
final String clientIp;
final String virtualHostKey;

SearchLogContext(final FessConfig fessConfig, final String[] roles, final String userCode, final String userId,
final HttpServletRequest request, final String clientIp, final String virtualHostKey) {
this.fessConfig = fessConfig;
this.roles = roles;
this.userCode = userCode;
this.userId = userId;
this.request = request;
this.clientIp = clientIp;
this.virtualHostKey = virtualHostKey;
}
}

/**
* Adds a search log to the queue.
*
Expand All @@ -143,19 +167,51 @@ public void addSearchLog(final SearchRequestParams params, final LocalDateTime r
return;
}

final RoleQueryHelper roleQueryHelper = ComponentUtil.getRoleQueryHelper();
final UserInfoHelper userInfoHelper = ComponentUtil.getUserInfoHelper();
final SearchLogContext context = createSearchLogContext(params, fessConfig);
createSearchLog(params, requestedTime, queryId, query, pageStart, pageSize, queryResponseList, context);
}

/**
* Resolves the runtime dependencies needed to build a SearchLog.
*
* @param params The search request parameters.
* @param fessConfig The Fess configuration.
* @return The resolved search log context.
*/
protected SearchLogContext createSearchLogContext(final SearchRequestParams params, final FessConfig fessConfig) {
final String[] roles = ComponentUtil.getRoleQueryHelper().build(params.getType()).stream().toArray(n -> new String[n]);
final String userCode = fessConfig.isUserInfo() ? ComponentUtil.getUserInfoHelper().getUserCode() : null;
final String userId = ComponentUtil.getRequestManager().findUserBean(FessUserBean.class).map(FessUserBean::getUserId).orElse(null);
final HttpServletRequest request = LaRequestUtil.getOptionalRequest().orElse(null);
final String clientIp = request != null ? ComponentUtil.getViewHelper().getClientIp(request) : null;
final String virtualHostKey = ComponentUtil.getVirtualHostHelper().getVirtualHostKey();

return new SearchLogContext(fessConfig, roles, userCode, userId, request, clientIp, virtualHostKey);
}

/**
* Builds a SearchLog from the given parameters and context, then adds it to the queue.
*
* @param params The search request parameters.
* @param requestedTime The time the search was requested.
* @param queryId The ID of the search query.
* @param query The search query string.
* @param pageStart The start position of the page.
* @param pageSize The size of the page.
* @param queryResponseList The list of query responses.
* @param context The search log context holding resolved dependencies.
*/
protected void createSearchLog(final SearchRequestParams params, final LocalDateTime requestedTime, final String queryId,
final String query, final int pageStart, final int pageSize, final QueryResponseList queryResponseList,
final SearchLogContext context) {
final SearchLog searchLog = new SearchLog();

if (fessConfig.isUserInfo()) {
final String userCode = userInfoHelper.getUserCode();
if (userCode != null) {
searchLog.setUserSessionId(userCode);
searchLog.setUserInfo(getUserInfo(userCode));
}
if (context.userCode != null) {
searchLog.setUserSessionId(context.userCode);
searchLog.setUserInfo(getUserInfo(context.userCode));
}

searchLog.setRoles(roleQueryHelper.build(params.getType()).stream().toArray(n -> new String[n]));
searchLog.setRoles(context.roles);
searchLog.setQueryId(queryId);
searchLog.setHitCount(queryResponseList.getAllRecordCount());
searchLog.setHitCountRelation(queryResponseList.getAllRecordCountRelation());
Expand All @@ -166,64 +222,75 @@ public void addSearchLog(final SearchRequestParams params, final LocalDateTime r
searchLog.setSearchQuery(StringUtils.abbreviate(queryResponseList.getSearchQuery(), 1000));
searchLog.setQueryOffset(pageStart);
searchLog.setQueryPageSize(pageSize);
ComponentUtil.getRequestManager().findUserBean(FessUserBean.class).ifPresent(user -> {
searchLog.setUser(user.getUserId());
});

LaRequestUtil.getOptionalRequest().ifPresent(req -> {
searchLog.setClientIp(StringUtils.abbreviate(ComponentUtil.getViewHelper().getClientIp(req), 100));
searchLog.setReferer(StringUtils.abbreviate(req.getHeader("referer"), 1000));
searchLog.setUserAgent(StringUtils.abbreviate(req.getHeader("user-agent"), 255));
final Object accessType = req.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE);
if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) {
searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
} else if (Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(accessType)) {
searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_GSA);
} else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) {
searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER);
} else if (Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(accessType)) {
searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN);
} else {
searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_WEB);
}
final Object languages = req.getAttribute(Constants.REQUEST_LANGUAGES);
if (context.userId != null) {
searchLog.setUser(context.userId);
}

if (context.request != null) {
searchLog.setClientIp(StringUtils.abbreviate(context.clientIp, 100));
searchLog.setReferer(StringUtils.abbreviate(context.request.getHeader("referer"), 1000));
searchLog.setUserAgent(StringUtils.abbreviate(context.request.getHeader("user-agent"), 255));

searchLog.setAccessType(determineAccessType(context.request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE)));

final Object languages = context.request.getAttribute(Constants.REQUEST_LANGUAGES);
if (languages != null) {
searchLog.setLanguages(StringUtils.join((String[]) languages, ","));
} else {
searchLog.setLanguages(StringUtil.EMPTY);
}

@SuppressWarnings("unchecked")
final Map<String, List<String>> fieldLogMap = (Map<String, List<String>>) req.getAttribute(Constants.FIELD_LOGS);
final Map<String, List<String>> fieldLogMap = (Map<String, List<String>>) context.request.getAttribute(Constants.FIELD_LOGS);
if (fieldLogMap != null) {
final int queryMaxLength = fessConfig.getQueryMaxLengthAsInteger();
final int queryMaxLength = context.fessConfig.getQueryMaxLengthAsInteger();
for (final Map.Entry<String, List<String>> logEntry : fieldLogMap.entrySet()) {
for (final String value : logEntry.getValue()) {
searchLog.addSearchFieldLogValue(logEntry.getKey(), StringUtils.abbreviate(value, queryMaxLength));
}
}
}

for (final String s : fessConfig.getSearchlogRequestHeadersAsArray()) {
for (final String s : context.fessConfig.getSearchlogRequestHeadersAsArray()) {
final String key = s.replace('-', '_').toLowerCase(Locale.ENGLISH);
Collections.list(req.getHeaders(s)).stream().forEach(v -> {
Collections.list(context.request.getHeaders(s)).stream().forEach(v -> {
searchLog.addRequestHeaderValue(key, v);
});
}
});
}

final String virtualHostKey = ComponentUtil.getVirtualHostHelper().getVirtualHostKey();
if (StringUtil.isNotBlank(virtualHostKey)) {
searchLog.setVirtualHost(virtualHostKey);
if (StringUtil.isNotBlank(context.virtualHostKey)) {
searchLog.setVirtualHost(context.virtualHostKey);
} else {
searchLog.setVirtualHost(StringUtil.EMPTY);
}

addDocumentsInResponse(queryResponseList, searchLog);

searchLogQueue.add(searchLog);
}

/**
* Returns the access type string from the given request attribute value, defaulting to web.
*
* @param accessType The access type attribute value from the request.
* @return The access type string.
*/
protected String determineAccessType(final Object accessType) {
if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) {
return Constants.SEARCH_LOG_ACCESS_TYPE_JSON;
} else if (Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(accessType)) {
return Constants.SEARCH_LOG_ACCESS_TYPE_GSA;
} else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) {
return Constants.SEARCH_LOG_ACCESS_TYPE_OTHER;
} else if (Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(accessType)) {
return Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN;
} else if (accessType instanceof String && StringUtil.isNotBlank((String) accessType)) {
return (String) accessType;
}
return Constants.SEARCH_LOG_ACCESS_TYPE_WEB;
}

/**
* Adds documents in the response to the search log.
*
Expand Down
111 changes: 111 additions & 0 deletions src/test/java/org/codelibs/fess/api/chat/ChatApiManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;

import org.codelibs.fess.Constants;
import org.codelibs.fess.entity.ChatMessage.ChatSource;
import org.codelibs.fess.entity.FacetQueryView;
import org.codelibs.fess.helper.ViewHelper;
Expand Down Expand Up @@ -371,6 +372,105 @@ public void test_createSuccessResponse_emptySources() {
assertEquals(sources, response.get("sources"));
}

// ===== getMaxMessageLength tests =====

@Test
public void test_getMaxMessageLength_default() {
ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() {
private static final long serialVersionUID = 1L;

@Override
public String getOrDefault(final String key, final String defaultValue) {
if ("rag.chat.message.max.length".equals(key)) {
return defaultValue;
}
return defaultValue;
}
});

final int result = chatApiManager.getMaxMessageLength(ComponentUtil.getFessConfig());
assertEquals(4000, result);
}

@Test
public void test_getMaxMessageLength_customValue() {
ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() {
private static final long serialVersionUID = 1L;

@Override
public String getOrDefault(final String key, final String defaultValue) {
if ("rag.chat.message.max.length".equals(key)) {
return "8000";
}
return defaultValue;
}
});

final int result = chatApiManager.getMaxMessageLength(ComponentUtil.getFessConfig());
assertEquals(8000, result);
}

@Test
public void test_getMaxMessageLength_invalidValue() {
ComponentUtil.setFessConfig(new FessConfig.SimpleImpl() {
private static final long serialVersionUID = 1L;

@Override
public String getOrDefault(final String key, final String defaultValue) {
if ("rag.chat.message.max.length".equals(key)) {
return "not-a-number";
}
return defaultValue;
}
});

final int result = chatApiManager.getMaxMessageLength(ComponentUtil.getFessConfig());
assertEquals(4000, result);
}

// ===== Access Type attribute tests =====

@Test
public void test_mockRequest_attributeStorage() {
final MockHttpServletRequest request = new MockHttpServletRequest();
assertNull(request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE));

request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, "ollama");
assertEquals("ollama", request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE));
}

@Test
public void test_mockRequest_attributeOverwrite() {
final MockHttpServletRequest request = new MockHttpServletRequest();

request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, "ollama");
assertEquals("ollama", request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE));

request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, "openai");
assertEquals("openai", request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE));
}

@Test
public void test_accessType_constantValues() {
assertEquals("json", Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
assertEquals("gsa", Constants.SEARCH_LOG_ACCESS_TYPE_GSA);
assertEquals("web", Constants.SEARCH_LOG_ACCESS_TYPE_WEB);
assertEquals("admin", Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN);
assertEquals("other", Constants.SEARCH_LOG_ACCESS_TYPE_OTHER);
}

@Test
public void test_accessType_llmNamesAreDifferentFromBuiltinTypes() {
final String[] llmNames = { "ollama", "openai", "gemini" };
for (final String llmName : llmNames) {
assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(llmName));
assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_GSA.equals(llmName));
assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_WEB.equals(llmName));
assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_ADMIN.equals(llmName));
assertFalse(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(llmName));
}
}

// ===== parseExtraQueries tests =====

private void setupViewHelperWithFacetGroups(FacetQueryView... views) {
Expand Down Expand Up @@ -549,6 +649,7 @@ private static class MockHttpServletRequest extends jakarta.servlet.http.HttpSer
private String servletPath;
private String method = "POST";
private final Map<String, String[]> parameterValuesMap = new HashMap<>();
private final Map<String, Object> attributeMap = new HashMap<>();

public MockHttpServletRequest() {
super(new MockServletRequest());
Expand Down Expand Up @@ -580,6 +681,16 @@ public String[] getParameterValues(String name) {
public void setParameterValues(String name, String[] values) {
parameterValuesMap.put(name, values);
}

@Override
public Object getAttribute(String name) {
return attributeMap.get(name);
}

@Override
public void setAttribute(String name, Object o) {
attributeMap.put(name, o);
}
}

/**
Expand Down
Loading
Loading