Highlights

Another feature called highlights has been added to RavenDB to enhance the search UX.

Usage

Lets consider a class and index as follows:

@QueryEntity
public class BlogPost {

  private String id;
  private String title;
  private String category;
  private String content;
  private Date publishedAt;
  private String[] tags;
  private List<BlogComment> comments;

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getCategory() {
    return category;
  }

  public void setCategory(String category) {
    this.category = category;
  }

  public String getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = content;
  }

  public Date getPublishedAt() {
    return publishedAt;
  }

  public void setPublishedAt(Date publishedAt) {
    this.publishedAt = publishedAt;
  }

  public String[] getTags() {
    return tags;
  }

  public void setTags(String[] tags) {
    this.tags = tags;
  }

  public List<BlogComment> getComments() {
    return comments;
  }

  public void setComments(List<BlogComment> comments) {
    this.comments = comments;
  }
}

public class BlogComment {
  private String title;
  private String content;

  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }
}

public static class BlogPosts_ByContent extends AbstractIndexCreationTask {
  public BlogPosts_ByContent() {
    QBlogPost b = QBlogPost.blogPost;
    map =
     " from post in posts " +
     " select new         " +
     "  {                 " +
     "      post.Content  " +
     "  };";
    index(b.content, FieldIndexing.ANALYZED);
    store(b.content, FieldStorage.YES);
    termVector(b.content, FieldTermVector.WITH_POSITIONS_AND_OFFSETS);
  }
}

Now to use Highlights we just need to use one of the highlight query methods. The basic usage can be as simple as:

Reference<FieldHighlightings> highlightingsRef = new Reference<>();

List<BlogPost> results = session
  .advanced()
  .documentQuery(BlogPost.class, BlogPosts_ByContent.class)
  .highlight("Content", 128, 1, highlightingsRef)
  .search("Content", "raven")
  .toList();

StringBuilder builder = new StringBuilder();
builder.append("<ul>\n");
for (BlogPost result : results) {
  String[] fragments = highlightingsRef.value.getFragments(result.getId());
  builder.append("<li>");
  builder.append(fragments[0]);
  builder.append("</li>");
}

String ul = builder.append("</ul>").toString();

This will return the list of results and for each result we will be displaying first found fragment with the length up to 128 characters.

Highlights + Projections

Highlights can also be accessed when projections are performed.

Reference<FieldHighlightings> highlightingsRef = new Reference<>();

List<BlogPosts_ByCategory_Content.Result> results = session
  .advanced()
  .documentQuery(BlogPost.class, BlogPosts_ByCategory_Content.class)
  .highlight("Content", 128, 1, highlightingsRef)
  .setHighlighterTags("**", "**")
  .search("Content", "raven")
  .selectFields(BlogPosts_ByCategory_Content.Result.class)
  .toList();

Highlights + Map-Reduce

Highlights can be accessed when performing queries on map-reduce indexes.

public static class BlogPosts_ByCategory_Content extends AbstractIndexCreationTask {

  public static class Result {
    private String category;
    private String content;

    public String getCategory() {
      return category;
    }
    public void setCategory(String category) {
      this.category = category;
    }
    public String getContent() {
      return content;
    }
    public void setContent(String content) {
      this.content = content;
    }
  }

  public BlogPosts_ByCategory_Content() {
    QBlogPost b = QBlogPost.blogPost;
    map =
     " from post in posts " +
     " select new         " +
     "  {                 " +
     "      post.Category, " +
     "      post.Content  " +
     "  };";

    reduce =
      " from result in results " +
      " group result by result.Category into g " +
      " select new " +
      " { " +
      "    Category = g.Key, " +
      "    Content = string.Join(\" \", g.Select(r => r.Content " +
      " }";
    index(b.content, FieldIndexing.ANALYZED);
    store(b.content, FieldStorage.YES);
    termVector(b.content, FieldTermVector.WITH_POSITIONS_AND_OFFSETS);
  }
}

Reference<FieldHighlightings> highlightingsRef = new Reference<>();

List<BlogPosts_ByCategory_Content.Result> results = session
  .advanced()
  .documentQuery(BlogPosts_ByCategory_Content.Result.class, BlogPosts_ByCategory_Content.class)
  .highlight("Content", "Category", 128, 1, highlightingsRef) // highlighting 'Content', but marking 'Category' as key
  .setHighlighterTags("**", "**")
  .search("Content", "raven")
  .selectFields(BlogPosts_ByCategory_Content.Result.class)
  .toList();

String[] newsHighlightings = highlightingsRef.value.getFragments("News"); // get fragments for 'News' category

Customization

public TSelf highlight(String fieldName, int fragmentLength, int fragmentCount, String fragmentsField);

public TSelf highlight(String fieldName, int fragmentLength, int fragmentCount, Reference<FieldHighlightings> highlightings);

public <TValue> TSelf highlight(Expression<?> propertySelector, int fragmentLength, int fragmentCount, ListPath<?, ?> fragmentsPropertySelector);

public <TValue> TSelf highlight(Expression<?> propertySelector, int fragmentLength, int fragmentCount, Reference<FieldHighlightings> highlightings);

where:
* fieldName or propertySelector is used to mark a field/property for highlight.
* fragmentLength this is the maximum length of text fragments that will be returned.
* fragmentCount this is the maximum number of fragments that will be returned.
* highlightings this will return an instance of a FieldHighlightings that contains the highlight fragments for each returned result.

By default, the highlighted text is wrapped with <b></b> tags, to change this behavior the setHighlighterTags method was introduced.

public TSelf setHighlighterTags(String preTag, String postTag);

public TSelf setHighlighterTags(String[] preTags, String[] postTags);

Example. To wrap highlighted text with ** we just need to execute following query:

Reference<FieldHighlightings> highlightingsRef = new Reference<>();

List<BlogPost> results = session
  .advanced()
  .documentQuery(BlogPost.class, BlogPosts_ByContent.class)
  .highlight("Content", 128, 1, highlightingsRef)
  .setHighlighterTags("**", "**")
  .search("Content", "raven")
  .toList();

Note

Default <b></b> tags are coloured and colours are returned in following order:

  •  yellow,
  •  lawngreen,
  •  aquamarine,
  •  magenta,
  •  palegreen,
  •  coral,
  •  wheat,
  •  khaki,
  •  lime,
  •  deepskyblue,
  •  deeppink,
  •  salmon,
  •  peachpuff,
  •  violet,
  •  mediumpurple,
  •  palegoldenrod,
  •  darkkhaki,
  •  springgreen,
  •  turquoise,
  •  powderblue