forked from Mirrors/privacore-open-source-search-engine
1149 lines
30 KiB
C++
1149 lines
30 KiB
C++
#include "Summary.h"
|
|
|
|
#include "Words.h"
|
|
#include "Sections.h"
|
|
#include "Query.h"
|
|
#include "Xml.h"
|
|
#include "Pos.h"
|
|
#include "Matches.h"
|
|
#include "Process.h"
|
|
#include "Conf.h"
|
|
#include "Url.h"
|
|
#include "Mem.h"
|
|
|
|
|
|
Summary::Summary()
|
|
: m_summaryLen(0)
|
|
, m_numExcerpts(0)
|
|
, m_numDisplayLines(0)
|
|
, m_displayLen(0)
|
|
, m_maxNumCharsPerLine(0)
|
|
, m_isSetFromTags(false)
|
|
, m_q(NULL)
|
|
, m_wordWeights(NULL)
|
|
, m_wordWeightSize(0)
|
|
, m_buf4(NULL)
|
|
, m_buf4Size(0) {
|
|
|
|
// PVS-Studio
|
|
m_summary[0] = '\0';
|
|
memset(m_summaryExcerptLen, 0, sizeof(m_summaryExcerptLen));
|
|
m_tmpWordWeightsBuf[0] = '\0';
|
|
m_tmpBuf4[0] = '\0';
|
|
}
|
|
|
|
Summary::~Summary() {
|
|
if ( m_wordWeights && m_wordWeights != (float *)m_tmpWordWeightsBuf ) {
|
|
mfree ( m_wordWeights , m_wordWeightSize , "sumww");
|
|
m_wordWeights = NULL;
|
|
}
|
|
|
|
if ( m_buf4 && m_buf4 != m_tmpBuf4 ) {
|
|
mfree ( m_buf4 , m_buf4Size , "ssstkb" );
|
|
m_buf4 = NULL;
|
|
}
|
|
}
|
|
|
|
char* Summary::getSummary() {
|
|
return m_summary;
|
|
}
|
|
|
|
const char *Summary::getSummary() const {
|
|
return m_summary;
|
|
}
|
|
|
|
int32_t Summary::getSummaryDisplayLen() const {
|
|
return m_displayLen;
|
|
}
|
|
|
|
int32_t Summary::getSummaryLen() const {
|
|
return m_summaryLen;
|
|
}
|
|
|
|
bool Summary::isSetFromTags() const {
|
|
return m_isSetFromTags;
|
|
}
|
|
|
|
bool Summary::verifySummary(const char *titleBuf, int32_t titleBufLen) {
|
|
logTrace(g_conf.m_logTraceSummary, "BEGIN");
|
|
|
|
if ( m_summaryLen > 0 ) {
|
|
// trim elipsis
|
|
if ( ( titleBufLen > 4 ) && ( memcmp( (titleBuf + titleBufLen - 4), " ...", 4 ) == 0 ) ) {
|
|
titleBufLen -= 4;
|
|
}
|
|
|
|
// verify that it's not the same with title
|
|
if ( strncasestr( m_summary, titleBuf, m_summaryLen, titleBufLen ) ) {
|
|
m_summaryLen = 0;
|
|
m_summary[0] = '\0';
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning false");
|
|
return false;
|
|
}
|
|
|
|
m_summaryExcerptLen[0] = m_summaryLen;
|
|
m_numExcerpts = 1;
|
|
m_displayLen = m_summaryLen;
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning true");
|
|
return true;
|
|
}
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning false");
|
|
return false;
|
|
}
|
|
|
|
void Summary::setSummary(const std::string &summary) {
|
|
m_summaryLen = summary.copy(m_summary, MAX_SUMMARY_LEN);
|
|
m_summary[m_summaryLen] = '\0';
|
|
m_summaryExcerptLen[0] = m_summaryLen;
|
|
m_numExcerpts = 1;
|
|
m_displayLen = m_summaryLen;
|
|
}
|
|
|
|
// let's try to get a nicer summary by using what the website set as description
|
|
// Use the following in priority order (highest first)
|
|
// - itemprop = "description"
|
|
// - meta name = "og:description"
|
|
// - meta name = "description"
|
|
bool Summary::setSummaryFromTags( Xml *xml, int32_t maxSummaryLen, const char *titleBuf, int32_t titleBufLen ) {
|
|
logTrace(g_conf.m_logTraceSummary, "BEGIN");
|
|
|
|
// sanity check
|
|
if ( maxSummaryLen >= MAX_SUMMARY_LEN ) {
|
|
g_errno = EBUFTOOSMALL;
|
|
|
|
logDebug(g_conf.m_logDebugSummary, "sum: summary too big to hold in buffer of %" PRId32" bytes.",(int32_t)MAX_SUMMARY_LEN);
|
|
logTrace(g_conf.m_logTraceSummary, "END. maxSummaryLen[%d] >= MAX_SUMMARY_LEN[%d]. Returning false", maxSummaryLen, MAX_SUMMARY_LEN);
|
|
|
|
return false;
|
|
}
|
|
|
|
/// @todo ALC configurable minSummaryLen so we can tweak this as needed
|
|
const int minSummaryLen = (maxSummaryLen / 3);
|
|
|
|
// itemprop = "description"
|
|
if ( xml->getTagContent("itemprop", "description", m_summary, MAX_SUMMARY_LEN, minSummaryLen, maxSummaryLen, &m_summaryLen) ) {
|
|
if ( verifySummary( titleBuf, titleBufLen ) ) {
|
|
m_isSetFromTags = true;
|
|
|
|
logDebug(g_conf.m_logDebugSummary, "sum: generated from itemprop description. summary='%.*s'", m_summaryLen, m_summary);
|
|
logTrace(g_conf.m_logTraceSummary, "END. Generated from itemprop description. Returning true");
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// meta property = "og:description"
|
|
if ( xml->getTagContent("property", "og:description", m_summary, MAX_SUMMARY_LEN, minSummaryLen, maxSummaryLen, &m_summaryLen, true, TAG_META ) ) {
|
|
if ( verifySummary( titleBuf, titleBufLen ) ) {
|
|
m_isSetFromTags = true;
|
|
|
|
logDebug(g_conf.m_logDebugSummary, "sum: generated from meta property og:description. summary='%.*s'", m_summaryLen, m_summary);
|
|
logTrace(g_conf.m_logTraceSummary, "END. Generated from meta property og:description Returning true");
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// meta name = "description"
|
|
if ( xml->getTagContent("name", "description", m_summary, MAX_SUMMARY_LEN, minSummaryLen, maxSummaryLen, &m_summaryLen, true, TAG_META ) ) {
|
|
if ( verifySummary( titleBuf, titleBufLen ) ) {
|
|
m_isSetFromTags = true;
|
|
|
|
logDebug(g_conf.m_logDebugSummary, "sum: generated from meta name description. summary='%.*s'", m_summaryLen, m_summary);
|
|
logTrace(g_conf.m_logTraceSummary, "END. Generated from meta name description. Returning true");
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
logDebug(g_conf.m_logDebugSummary, "sum: unable to generate summary from itemprop/meta tags");
|
|
logTrace(g_conf.m_logTraceSummary, "END. Unable to generate summary. Returning false");
|
|
|
|
return false;
|
|
}
|
|
|
|
// returns false and sets g_errno on error
|
|
bool Summary::setSummary(const Xml *xml, const Words *words, const Sections *sections, Pos *pos, const Query *q, int32_t maxSummaryLen,
|
|
int32_t maxNumLines, int32_t numDisplayLines, int32_t maxNumCharsPerLine, const Url *f,
|
|
const Matches *matches, const char *titleBuf, int32_t titleBufLen) {
|
|
logTrace(g_conf.m_logTraceSummary, "BEGIN");
|
|
|
|
m_numDisplayLines = numDisplayLines;
|
|
m_displayLen = 0;
|
|
|
|
if ( maxNumCharsPerLine < 10 ) {
|
|
maxNumCharsPerLine = 10;
|
|
}
|
|
|
|
// . sanity check
|
|
// . summary must fit in m_summary[]
|
|
// . leave room for tailing \0
|
|
if ( maxSummaryLen >= MAX_SUMMARY_LEN ) {
|
|
g_errno = EBUFTOOSMALL;
|
|
log(LOG_WARN, "query: Summary too big to hold in buffer of %" PRId32" bytes.",(int32_t)MAX_SUMMARY_LEN);
|
|
logTrace(g_conf.m_logTraceSummary, "END. maxSummaryLen[%d] >= MAX_SUMMARY_LEN[%d]. Returning false", maxSummaryLen, MAX_SUMMARY_LEN);
|
|
return false;
|
|
}
|
|
|
|
// do not overrun the final*[] buffers
|
|
if ( maxNumLines > 256 ) {
|
|
g_errno = EBUFTOOSMALL;
|
|
log(LOG_WARN, "query: More than 256 summary lines requested.");
|
|
logTrace(g_conf.m_logTraceSummary, "END. maxNumLines[%d] > 256. Returning false", maxNumLines);
|
|
return false;
|
|
}
|
|
|
|
// Nothing to match...print beginning of content as summary
|
|
if ( matches->getNumMatches() == 0 && maxNumLines > 0 ) {
|
|
bool status = getDefaultSummary(xml, words, sections, pos, maxSummaryLen);
|
|
logTrace(g_conf.m_logTraceSummary, "END. getDefaultSummary. Returning %s", status ? "true" : "false");
|
|
return status;
|
|
}
|
|
|
|
int32_t need1 = q->m_numWords * sizeof(float);
|
|
m_wordWeightSize = need1;
|
|
if ( need1 < 128 ) {
|
|
m_wordWeights = (float *)m_tmpWordWeightsBuf;
|
|
} else {
|
|
m_wordWeights = (float *)mmalloc ( need1 , "wwsum" );
|
|
}
|
|
|
|
if ( ! m_wordWeights ) {
|
|
logTrace(g_conf.m_logTraceSummary, "END. !m_wordWeights. Returning false");
|
|
return false;
|
|
}
|
|
|
|
/// @todo ALC fix word weights
|
|
/// non-working logic is removed in commit 5eacee9063861e859b54ec62035a600aa8af25df
|
|
|
|
// . compute our word weights wrt each query. words which are more rare
|
|
// have a higher weight. We use this to weight the terms importance
|
|
// when generating the summary.
|
|
// . used by the proximity algo
|
|
// . used in setSummaryScores() for scoring summaries
|
|
|
|
|
|
for ( int32_t i = 0 ; i < q->m_numWords; i++ ) {
|
|
m_wordWeights[i] = 1.0;
|
|
}
|
|
|
|
// convenience
|
|
m_maxNumCharsPerLine = maxNumCharsPerLine;
|
|
m_q = q;
|
|
|
|
// set the max excerpt len to the max summary excerpt len
|
|
int32_t maxExcerptLen = m_maxNumCharsPerLine;
|
|
|
|
int32_t lastNumFinal = 0;
|
|
int32_t maxLoops = 1024;
|
|
|
|
// if just computing absScore2...
|
|
if ( maxNumLines <= 0 ) {
|
|
logTrace(g_conf.m_logTraceSummary, "END. maxNumLines <= 0. Returning true");
|
|
return true;
|
|
}
|
|
|
|
char *p = m_summary;
|
|
char *pend = m_summary + maxSummaryLen;
|
|
|
|
m_numExcerpts = 0;
|
|
|
|
int32_t need2 = (1+1+1) * m_q->m_numWords;
|
|
m_buf4Size = need2;
|
|
if ( need2 < 128 ) {
|
|
m_buf4 = m_tmpBuf4;
|
|
} else {
|
|
m_buf4 = (char *)mmalloc ( need2 , "stkbuf" );
|
|
}
|
|
|
|
if ( ! m_buf4 ) {
|
|
logTrace(g_conf.m_logTraceSummary, "END. !m_buf4. Returning false");
|
|
return false;
|
|
}
|
|
|
|
char *x = m_buf4;
|
|
char *retired = x;
|
|
x += m_q->m_numWords;
|
|
char *maxGotIt = x;
|
|
x += m_q->m_numWords;
|
|
char *gotIt = x;
|
|
|
|
// . the "maxGotIt" count vector accumulates into "retired"
|
|
// . that is how we keep track of what query words we used for previous
|
|
// summary excerpts so we try to get diversified excerpts with
|
|
// different query terms/words in them
|
|
//char retired [ MAX_QUERY_WORDS ];
|
|
memset ( retired, 0, m_q->m_numWords * sizeof(char) );
|
|
|
|
// some query words are already matched in the title
|
|
for ( int32_t i = 0 ; i < m_q->m_numWords ; i++ ) {
|
|
if ( matches->isTitleMatch(i) ) {
|
|
retired [ i ] = 1;
|
|
}
|
|
}
|
|
|
|
bool hadEllipsis = false;
|
|
|
|
//
|
|
// Loop over all words that match a query term. The matching words
|
|
// could be from any one of the 3 Words arrays above. Find the
|
|
// highest scoring window around each term. And then find the highest
|
|
// of those over all the matching terms.
|
|
//
|
|
int32_t numFinal;
|
|
for ( numFinal = 0; numFinal < maxNumLines; numFinal++ ) {
|
|
if ( numFinal == m_numDisplayLines ) {
|
|
m_displayLen = p - m_summary;
|
|
}
|
|
|
|
// reset these at the top of each loop
|
|
const Match *maxm;
|
|
int64_t maxScore = 0;
|
|
int32_t maxa = 0;
|
|
int32_t maxb = 0;
|
|
int32_t maxi = -1;
|
|
int32_t lasta = -1;
|
|
|
|
if(lastNumFinal == numFinal) {
|
|
if(maxLoops-- <= 0) {
|
|
log(LOG_WARN, "query: got infinite loop bug, query is %s url is %s", m_q->originalQuery(), f->getUrl());
|
|
break;
|
|
}
|
|
}
|
|
lastNumFinal = numFinal;
|
|
|
|
// loop through all the matches and see which is best
|
|
for ( int32_t i = 0 ; i < matches->getNumMatches() ; i++ ) {
|
|
int32_t a , b;
|
|
// reset lasta if we changed words class
|
|
if ( i > 0 && matches->getMatch(i-1).m_words != matches->getMatch(i).m_words ) {
|
|
lasta = -1;
|
|
}
|
|
|
|
// only use matches in title, etc.
|
|
mf_t flags = matches->getMatch(i).m_flags;
|
|
|
|
bool skip = true;
|
|
if ( flags & MF_METASUMM ) {
|
|
skip = false;
|
|
}
|
|
if ( flags & MF_METADESC ) {
|
|
skip = false;
|
|
}
|
|
if ( flags & MF_BODY ) {
|
|
skip = false;
|
|
}
|
|
if ( flags & MF_RSSDESC ) {
|
|
skip = false;
|
|
}
|
|
|
|
if ( skip ) {
|
|
continue;
|
|
}
|
|
|
|
// ask him for the query words he matched
|
|
//char gotIt [ MAX_QUERY_WORDS ];
|
|
// clear it for him
|
|
memset ( gotIt, 0, m_q->m_numWords * sizeof(char) );
|
|
|
|
// . get score of best window around this match
|
|
// . do not allow left post of window to be <= lasta to
|
|
// avoid repeating the same window.
|
|
int64_t score = getBestWindow (matches, i, &lasta, &a, &b, gotIt, retired, maxExcerptLen);
|
|
|
|
// USE THIS BUF BELOW TO DEBUG THE ABOVE CODE.
|
|
// PRINTS OUT THE SUMMARY
|
|
/*
|
|
//if ( score >=12000 ) {
|
|
char buf[10*1024];
|
|
char *xp = buf;
|
|
if ( i == 0 )
|
|
log (LOG_WARN,"=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
|
|
sprintf(xp, "score=%08" PRId32" a=%05" PRId32" b=%05" PRId32" ",
|
|
(int32_t)score,(int32_t)a,(int32_t)b);
|
|
xp += strlen(xp);
|
|
for ( int32_t j = a; j < b; j++ ){
|
|
//int32_t s = scores->m_scores[j];
|
|
int32_t s = 0;
|
|
if ( s < 0 ) continue;
|
|
char e = 1;
|
|
int32_t len = words->getWordLen(j);
|
|
for(int32_t k=0;k<len;k +=e){
|
|
char c = words->m_words[j][k];
|
|
//if ( is_binary( c ) ) continue;
|
|
*xp = c;
|
|
xp++;
|
|
}
|
|
//p += strlen(p);
|
|
if ( s == 0 ) continue;
|
|
sprintf ( xp ,"(%" PRId32")",s);
|
|
xp += strlen(xp);
|
|
}
|
|
log (LOG_WARN,"query: summary: %s", buf);
|
|
//}
|
|
*/
|
|
|
|
// prints out the best window with the score
|
|
/*
|
|
char buf[MAX_SUMMARY_LEN];
|
|
char *bufPtr = buf;
|
|
char *bufPtrEnd = p + MAX_SUMMARY_LEN;
|
|
if ( i == 0 )
|
|
log (LOG_WARN,"=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
|
|
int32_t len = 0;
|
|
Words *ww = matches->m_matches[i].m_words;
|
|
//Sections *ss = matches->m_matches[i].m_sections;
|
|
//if ( ss->m_numSections <= 0 ) ss = NULL;
|
|
//len=pos->filter(bufPtr, bufPtrEnd, ww, a, b, NULL);
|
|
//log(LOG_WARN,"summary: %" PRId32") %s - %" PRId64,i,bufPtr,
|
|
//score);
|
|
log(LOG_WARN,"summary: %" PRId32") %s - %" PRId64,i,bufPtr,
|
|
score);
|
|
*/
|
|
|
|
// skip if was in title or something
|
|
if ( score <= 0 ) {
|
|
continue;
|
|
}
|
|
|
|
// skip if not a winner
|
|
if ( maxi >= 0 && score <= maxScore ) {
|
|
continue;
|
|
}
|
|
|
|
// we got a new winner
|
|
maxi = i;
|
|
maxa = a;
|
|
maxb = b;
|
|
maxScore = score;
|
|
|
|
// save this too
|
|
gbmemcpy ( maxGotIt , gotIt , m_q->m_numWords );
|
|
|
|
}
|
|
|
|
// retire the query words in the winning summary
|
|
|
|
// all done if no winner was made
|
|
if ( maxi == -1 || maxa == -1 || maxb == -1) {
|
|
break;
|
|
}
|
|
|
|
// who is the winning match?
|
|
maxm = &matches->getMatch(maxi);
|
|
Words *ww = maxm->m_words;
|
|
|
|
// we now use "m_swbits" for the summary bits since they are
|
|
// of size sizeof(swbit_t), a int16_t at this point
|
|
swbit_t *bb = maxm->m_bits->m_swbits;
|
|
|
|
// this should be impossible
|
|
if ( maxa > ww->getNumWords() || maxb > ww->getNumWords() ) {
|
|
log ( LOG_WARN,"query: summary starts or ends after "
|
|
"document is over! maxa=%" PRId32" maxb=%" PRId32" nw=%" PRId32,
|
|
maxa, maxb, ww->getNumWords() );
|
|
maxa = ww->getNumWords() - 1;
|
|
maxb = ww->getNumWords();
|
|
}
|
|
|
|
// assume we do not preceed with ellipsis "..."
|
|
bool needEllipsis = true;
|
|
|
|
const char *c = ww->getWord(maxa)+0;
|
|
|
|
// rule of thumb, don't use ellipsis if the first letter is capital, or a non letter
|
|
// is punct word before us pair acrossable? if so then we probably are not the start of a sentence.
|
|
// or if into the sample and previous excerpt had an ellipsis do not bother using one for us.
|
|
if ( !is_alpha_utf8(c) || is_upper_utf8(c) ||
|
|
(bb[maxa] & D_STARTS_SENTENCE) ||
|
|
(p > m_summary && hadEllipsis)) {
|
|
needEllipsis = false;
|
|
}
|
|
|
|
if ( needEllipsis ) {
|
|
// break out if no room for "..."
|
|
if ( p + 4 + 2 > pend ) {
|
|
break;
|
|
}
|
|
|
|
// space first?
|
|
if ( p > m_summary ) {
|
|
*p++ = ' ';
|
|
}
|
|
|
|
memcpy ( p, "\342\200\246 ", 4 ); //horizontal ellipsis, code point 0x2026
|
|
p += 4;
|
|
}
|
|
|
|
// separate summary excerpts with a single space.
|
|
if ( p > m_summary ) {
|
|
if ( p + 2 > pend ) {
|
|
break;
|
|
}
|
|
|
|
*p++ = ' ';
|
|
}
|
|
|
|
// assume we need a trailing ellipsis
|
|
needEllipsis = true;
|
|
|
|
// so next excerpt does not need to have an ellipsis if we
|
|
// have one at the end of this excerpt
|
|
hadEllipsis = needEllipsis;
|
|
|
|
// start with quote?
|
|
if ( (bb[maxa] & D_IN_QUOTES) && p + 1 < pend ) {
|
|
// preceed with quote
|
|
*p++ = '\"';
|
|
}
|
|
|
|
// . filter the words into p
|
|
// . removes back to back spaces
|
|
// . converts html entities
|
|
// . filters in stores words in [a,b) interval
|
|
int32_t len = pos->filter( ww, maxa, maxb, false, p, pend, xml->getVersion() );
|
|
|
|
// break out if did not fit
|
|
if ( len == 0 ) {
|
|
break;
|
|
}
|
|
|
|
// don't consider it if it is a substring of the title
|
|
if ( len == titleBufLen && strncasestr(titleBuf, p, titleBufLen, len) ) {
|
|
// don't consider this one
|
|
numFinal--;
|
|
goto skip;
|
|
}
|
|
|
|
// don't consider it if the length wasn't anything nice
|
|
if ( len < 5 ){
|
|
numFinal--;
|
|
goto skip;
|
|
}
|
|
|
|
// otherwise, keep going
|
|
p += len;
|
|
|
|
// now we just indicate which query terms we got
|
|
for ( int32_t i = 0 ; i < m_q->m_numWords ; i++ ) {
|
|
// do not breach
|
|
if ( retired[i] >= 100 ) {
|
|
continue;
|
|
}
|
|
retired [ i ] += maxGotIt [ i ];
|
|
}
|
|
|
|
// add all the scores of the excerpts to the doc summary score.
|
|
// zero out scores of the winning sample so we don't get them
|
|
// again. use negative one billion to ensure that we don't get
|
|
// them again
|
|
for ( int32_t j = maxa ; j < maxb ; j++ ) {
|
|
// mark it as used
|
|
bb[j] |= D_USED;
|
|
}
|
|
|
|
// if we ended on punct that can be paired across we need
|
|
// to add an ellipsis
|
|
if ( needEllipsis ) {
|
|
if ( p + 4 + 2 > pend ) {
|
|
break;
|
|
}
|
|
memcpy ( p, " \342\200\246", 4 ); //horizontal ellipsis, code point 0x2026
|
|
p += 4;
|
|
}
|
|
|
|
// try to put in a small summary excerpt if we have atleast
|
|
// half of the normal excerpt length left
|
|
if ( maxExcerptLen == m_maxNumCharsPerLine && len <= ( m_maxNumCharsPerLine / 2 + 1 ) ) {
|
|
maxExcerptLen = m_maxNumCharsPerLine / 2;
|
|
|
|
// don't count it in the finals since we try to get a small excerpt
|
|
numFinal--;
|
|
} else if ( m_numExcerpts < MAX_SUMMARY_EXCERPTS && m_numExcerpts >= 0 ) {
|
|
m_summaryExcerptLen[m_numExcerpts] = p - m_summary;
|
|
m_numExcerpts++;
|
|
|
|
// also reset maxExcerptLen
|
|
maxExcerptLen = m_maxNumCharsPerLine;
|
|
}
|
|
|
|
skip:
|
|
// zero out the scores so they will not be used in others
|
|
for ( int32_t j = maxa ; j < maxb ; j++ ) {
|
|
// mark it
|
|
bb[j] |= D_USED;
|
|
}
|
|
}
|
|
|
|
if ( numFinal <= m_numDisplayLines ) {
|
|
m_displayLen = p - m_summary;
|
|
}
|
|
|
|
// free the mem we used if we allocated it
|
|
if ( m_buf4 && m_buf4 != m_tmpBuf4 ) {
|
|
mfree ( m_buf4 , m_buf4Size , "ssstkb" );
|
|
m_buf4 = NULL;
|
|
}
|
|
|
|
// If we still didn't find a summary, get the default summary
|
|
if ( p == m_summary ) {
|
|
bool status = getDefaultSummary ( xml, words, sections, pos, maxSummaryLen );
|
|
if ( m_numDisplayLines > 0 ) {
|
|
m_displayLen = m_summaryLen;
|
|
}
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Unable to get summary. getDefaultSummary. Returning %s", status ? "true" : "false");
|
|
return status;
|
|
}
|
|
|
|
// if we don't find a summary, theres no need to NULL terminate
|
|
*p++ = '\0';
|
|
|
|
// set length
|
|
m_summaryLen = p - m_summary;
|
|
|
|
if ( m_summaryLen > 50000 ) { g_process.shutdownAbort(true); }
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning true");
|
|
return true;
|
|
}
|
|
|
|
// . return the score of the highest-scoring window containing match #m
|
|
// . window is defined by the half-open interval [a,b) where a and b are
|
|
// word #'s in the Words array indicated by match #m
|
|
// . return -1 and set g_errno on error
|
|
int64_t Summary::getBestWindow(const Matches *matches, int32_t mm, int32_t *lasta,
|
|
int32_t *besta, int32_t *bestb, char *gotIt,
|
|
char *retired, int32_t maxExcerptLen) {
|
|
logTrace(g_conf.m_logTraceSummary, "BEGIN");
|
|
|
|
// get the window around match #mm
|
|
const Match *m = &matches->getMatch(mm);
|
|
|
|
// what is the word # of match #mm?
|
|
int32_t matchWordNum = m->m_wordNum;
|
|
|
|
// what Words/Pos/Bits classes is this match in?
|
|
Words *words = m->m_words;
|
|
Section **sp = NULL;
|
|
int32_t *pos = m->m_pos->m_pos;
|
|
|
|
// use "m_swbits" not "m_bits", that is what Bits::setForSummary() uses
|
|
const swbit_t *bb = m->m_bits->m_swbits;
|
|
|
|
// shortcut
|
|
if ( m->m_sections ) {
|
|
sp = m->m_sections->m_sectionPtrs;
|
|
}
|
|
|
|
int32_t nw = words->getNumWords();
|
|
int64_t *wids = words->getWordIds();
|
|
nodeid_t *tids = words->getTagIds();
|
|
|
|
// . sanity check
|
|
// . this prevents a core i've seen
|
|
if ( matchWordNum >= nw ) {
|
|
log("summary: got overflow condition for q=%s",m_q->originalQuery());
|
|
|
|
// assume no best window
|
|
*besta = -1;
|
|
*bestb = -1;
|
|
*lasta = matchWordNum;
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. matchWordNum[%d] >= nw[%d]. Returning 0", matchWordNum, nw);
|
|
return 0;
|
|
}
|
|
|
|
// . we NULLify the section ptrs if we already used the word in another summary.
|
|
int32_t badFlags = NOINDEXFLAGS|SEC_IN_TITLE;
|
|
if ( (bb[matchWordNum] & D_USED) || ( sp && (sp[matchWordNum]->m_flags & badFlags) ) ) {
|
|
// assume no best window
|
|
*besta = -1;
|
|
*bestb = -1;
|
|
*lasta = matchWordNum;
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. word is used/bad. Returning 0");
|
|
return 0;
|
|
}
|
|
|
|
// . "a" is the left fence post of the window (it is a word # in Words)
|
|
// . go to the left as far as we can
|
|
// . thus we decrement "a"
|
|
int32_t a = matchWordNum;
|
|
|
|
// "posa" is the character position of the END of word #a
|
|
int32_t posa = pos[a+1];
|
|
int32_t firstFrag = -1;
|
|
bool startOnQuote = false;
|
|
bool goodStart = false;
|
|
int32_t wordCount = 0;
|
|
|
|
// . decrease "a" as long as we stay within maxNumCharsPerLine
|
|
// . avoid duplicating windows by using "lasta", the last "a" of the
|
|
// previous call to getBestWindow(). This can happen if our last
|
|
// central query term was close to this one.
|
|
for ( ; a > 0 && posa - pos[a-1] < maxExcerptLen && a > *lasta; a-- ) {
|
|
// . don't include any "dead zone",
|
|
// . dead zones have already been used for the summary, and
|
|
// we are getting a second/third/... excerpt here now then
|
|
// stop if its the start of a sentence, too
|
|
// stop before title word
|
|
if ( (bb[a-1] & D_USED) || (bb[a] & D_STARTS_SENTENCE) || ( bb[a-1] & D_IN_TITLE )) {
|
|
goodStart = true;
|
|
break;
|
|
}
|
|
|
|
// don't go beyond an LI, TR, P tag
|
|
if ( tids && ( tids[a-1] == TAG_LI ||
|
|
tids[a-1] == TAG_TR ||
|
|
tids[a-1] == TAG_P ||
|
|
tids[a-1] == TAG_DIV ) ) {
|
|
goodStart = true;
|
|
break;
|
|
}
|
|
|
|
// stop if its the start of a quoted sentence
|
|
if ( a+1<nw && (bb[a+1] & D_IN_QUOTES) &&
|
|
words->getWord(a)[0] == '\"' ){
|
|
startOnQuote = true;
|
|
goodStart = true;
|
|
break;
|
|
}
|
|
|
|
// find out the first instance of a fragment (comma, etc)
|
|
// watch out! because frag also means 's' in there's
|
|
if ( ( bb[a] & D_STARTS_FRAG ) && !(bb[a-1] & D_IS_STRONG_CONNECTOR) && firstFrag == -1 ) {
|
|
firstFrag = a;
|
|
}
|
|
|
|
if ( wids[a] ) {
|
|
wordCount++;
|
|
}
|
|
}
|
|
|
|
// if didn't find a good start, then start at the start of the frag
|
|
if ( !goodStart && firstFrag != -1 ) {
|
|
a = firstFrag;
|
|
}
|
|
|
|
// don't let punct or tag word start a line, unless a quote
|
|
if ( a < matchWordNum && !wids[a] && words->getWord(a)[0] != '\"' ){
|
|
while ( a < matchWordNum && !wids[a] ) a++;
|
|
|
|
// do not break right after a "strong connector", like
|
|
// apostrophe
|
|
while ( a < matchWordNum && a > 0 &&
|
|
( bb[a-1] & D_IS_STRONG_CONNECTOR ) )
|
|
a++;
|
|
|
|
// don't let punct or tag word start a line
|
|
while ( a < matchWordNum && !wids[a] ) a++;
|
|
}
|
|
|
|
// remember, b is not included in the summary, the summary is [a,b-1]
|
|
// remember to include all words in a matched phrase
|
|
int32_t b = matchWordNum + m->m_numWords ;
|
|
int32_t endQuoteWordNum = -1;
|
|
int32_t numTagsCrossed = 0;
|
|
|
|
for ( ; b <= nw; b++ ) {
|
|
if ( b == nw ) {
|
|
break;
|
|
}
|
|
|
|
if ( pos[b+1] - pos[a] >= maxExcerptLen ) {
|
|
break;
|
|
}
|
|
|
|
if ( startOnQuote && words->getWord(b)[0] == '\"' ) {
|
|
endQuoteWordNum = b;
|
|
}
|
|
|
|
// don't include any dead zone, those are already-used samples
|
|
if ( bb[b] & D_USED ) {
|
|
break;
|
|
}
|
|
|
|
// stop on a title word
|
|
if ( bb[b] & D_IN_TITLE ) {
|
|
break;
|
|
}
|
|
|
|
if ( wids[b] ) {
|
|
wordCount++;
|
|
}
|
|
|
|
// don't go beyond an LI or TR backtag
|
|
if ( tids && ( tids[b] == (BACKBIT|TAG_LI) ||
|
|
tids[b] == (BACKBIT|TAG_TR) ) ) {
|
|
numTagsCrossed++;
|
|
|
|
// try to have atleast 10 words in the summary
|
|
if ( wordCount > 10 ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// go beyond a P or DIV backtag in case the earlier char is a
|
|
// ':'. This came from a special case for wikipedia pages
|
|
// eg. http://en.wikipedia.org/wiki/Flyover
|
|
if ( tids && ( tids[b] == (BACKBIT|TAG_P) ||
|
|
tids[b] == (BACKBIT|TAG_DIV) )) {
|
|
numTagsCrossed++;
|
|
|
|
// try to have atleast 10 words in the summary
|
|
if ( wordCount > 10 && words->getWord(b-1)[0] != ':' ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't end on a lot of punct words
|
|
if ( b > matchWordNum && !wids[b-1]){
|
|
// remove more than one punct words. if we're ending on a quote
|
|
// keep it
|
|
while ( b > matchWordNum && !wids[b-2] && endQuoteWordNum != -1 && b > endQuoteWordNum ) {
|
|
b--;
|
|
}
|
|
|
|
// do not break right after a "strong connector", like apostrophe
|
|
while ( b > matchWordNum && (bb[b-2] & D_IS_STRONG_CONNECTOR) ) {
|
|
b--;
|
|
}
|
|
}
|
|
|
|
// make m_matches.m_matches[mi] the first match in our [a,b) window
|
|
int32_t mi ;
|
|
|
|
// . the match at the center of the window is match #"mm", so that
|
|
// matches->m_matches[mm] is the Match class
|
|
// . set "mi" to it and back up "mi" as long as >= a
|
|
for ( mi = mm ; mi > 0 && matches->getMatch(mi-1).m_wordNum >=a ; mi-- )
|
|
;
|
|
|
|
// now get the score of this excerpt. Also mark all the represented
|
|
// query words. Mark the represented query words in the array that
|
|
// comes to us. also mark how many times the same word is repeated in
|
|
// this summary.
|
|
int64_t score = 0LL;
|
|
|
|
// is a url contained in the summary, that looks bad! punish!
|
|
bool hasUrl = false;
|
|
|
|
// the word count we did above was just an approximate. count it right
|
|
wordCount = 0;
|
|
|
|
// for debug
|
|
//char buf[5000];
|
|
//char *xp = buf;
|
|
SafeBuf xp;
|
|
|
|
// wtf?
|
|
if ( b > nw ) {
|
|
b = nw;
|
|
}
|
|
|
|
// first score from the starting match down to a, including match
|
|
for ( int32_t i = a ; i < b ; i++ ) {
|
|
// debug print out
|
|
if ( g_conf.m_logDebugSummary ) {
|
|
int32_t len = words->getWordLen(i);
|
|
char cs;
|
|
for (int32_t k=0;k<len; k+=cs ) {
|
|
const char *c = words->getWord(i)+k;
|
|
cs = getUtf8CharSize(c);
|
|
if ( is_binary_utf8 ( c ) ) {
|
|
continue;
|
|
}
|
|
xp.safeMemcpy ( c , cs );
|
|
xp.nullTerm();
|
|
}
|
|
}
|
|
|
|
// skip if in bad section, marquee, select, script, style
|
|
if ( sp && (sp[i]->m_flags & badFlags) ) {
|
|
continue;
|
|
}
|
|
|
|
// don't count just numeric words
|
|
if ( words->isNum(i) ) {
|
|
continue;
|
|
}
|
|
|
|
// check if there is a url. best way to check for '://'
|
|
if ( wids && !wids[i] ) {
|
|
const char *wrd = words->getWord(i);
|
|
int32_t wrdLen = words->getWordLen(i);
|
|
if ( wrdLen == 3 && wrd[0] == ':' && wrd[1] == '/' && wrd[2] == '/' ) {
|
|
hasUrl = true;
|
|
}
|
|
}
|
|
|
|
// skip if not wid
|
|
if ( ! wids[i] ) {
|
|
continue;
|
|
}
|
|
|
|
// just make every word 100 pts
|
|
int32_t t = 100;
|
|
|
|
// penalize it if in one of these sections
|
|
if ( bb[i] & ( D_IN_PARENS | D_IN_SUP | D_IN_LIST ) ) {
|
|
t /= 2;
|
|
}
|
|
|
|
// boost it if in bold or italics
|
|
if ( bb[i] & D_IN_BOLDORITALICS ) {
|
|
t *= 2;
|
|
}
|
|
|
|
// add the score for this word
|
|
score += t;
|
|
|
|
// print the score, "t"
|
|
if ( g_conf.m_logDebugSummary ) {
|
|
xp.safePrintf("(%" PRId32")",t);
|
|
}
|
|
|
|
// count the alpha words we got
|
|
wordCount++;
|
|
|
|
// if no matches left, skip
|
|
if ( mi >= matches->getNumMatches() ) {
|
|
continue;
|
|
}
|
|
|
|
// get the match
|
|
const Match *next = &matches->getMatch(mi);
|
|
|
|
// skip if not a match
|
|
if ( i != next->m_wordNum ) {
|
|
continue;
|
|
}
|
|
|
|
// must be a match in this class
|
|
if ( next->m_words != words ) {
|
|
continue;
|
|
}
|
|
|
|
// advance it
|
|
mi++;
|
|
|
|
// which query word # does it match
|
|
int32_t qwn = next->m_qwordNum;
|
|
|
|
if ( qwn < 0 || qwn >= m_q->m_numWords ){g_process.shutdownAbort(true);}
|
|
|
|
// undo old score
|
|
score -= t;
|
|
|
|
// add 100000 per match
|
|
t = 100000;
|
|
|
|
// weight based on tf, goes from 0.1 to 1.0
|
|
t = (int32_t)((float)t * m_wordWeights [ qwn ]);
|
|
|
|
// if it is a query stop word, make it 10000 pts
|
|
if ( m_q->m_qwords[qwn].m_isQueryStopWord ) {
|
|
t = 0;//10000;
|
|
}
|
|
|
|
// penalize it if in one of these sections
|
|
if ( bb[i] & ( D_IN_PARENS | D_IN_SUP | D_IN_LIST ) ) {
|
|
t /= 2;
|
|
}
|
|
|
|
if ( gotIt[qwn] > 0 ) {
|
|
// have we matched it in this [a,b) already?
|
|
if ( gotIt[qwn] == 1 ) {
|
|
t /= 15;
|
|
} else {
|
|
// if we have more than 2 matches in the same window,
|
|
// it may not give a good summary. give a heavy penalty
|
|
t -= 200000;
|
|
}
|
|
} else if ( retired [qwn] > 0 ) {
|
|
// have we matched it already in a winning window?
|
|
t /= 12;
|
|
}
|
|
|
|
// add it back
|
|
score += t;
|
|
|
|
if ( g_conf.m_logDebugSummary ) {
|
|
xp.safePrintf ("[%" PRId32"]{qwn=%" PRId32",ww=%f}",t,qwn,
|
|
m_wordWeights[qwn]);
|
|
}
|
|
|
|
// inc the query word count for this window
|
|
if ( gotIt[qwn] < 100 ) {
|
|
gotIt[qwn]++;
|
|
}
|
|
}
|
|
|
|
int32_t oldScore = score;
|
|
|
|
// apply the bonus if it starts or a sentence
|
|
// only apply if the score is positive and if the wordcount is decent
|
|
if ( score > 0 && wordCount > 7 ){
|
|
// a match can give us 10k to 100k pts based on the tf weights
|
|
// so we don't want to overwhelm that too much, so let's make
|
|
// this a 20k bonus if it starts a sentence
|
|
if ( bb[a] & D_STARTS_SENTENCE ) {
|
|
score += 8000;
|
|
} else if ( bb[a] & D_STARTS_FRAG ) {
|
|
// likewise, a fragment, like after a comma
|
|
score += 4000;
|
|
}
|
|
|
|
// 1k if the match word is very close to the
|
|
// start of a sentence, lets say 3 alphawords
|
|
if ( matchWordNum - a < 7 ) {
|
|
score += 1000;
|
|
}
|
|
}
|
|
|
|
// a summary isn't really a summary if its less than 7 words.
|
|
// reduce the score, but still give it a decent score.
|
|
// minus 5M.
|
|
if ( wordCount < 7 ) {
|
|
score -= 20000;
|
|
}
|
|
|
|
// summaries that cross a lot of tags are usually bad, penalize them
|
|
if ( numTagsCrossed > 1 ) {
|
|
score -= (numTagsCrossed * 20000);
|
|
}
|
|
|
|
if ( hasUrl ) {
|
|
score -= 8000;
|
|
}
|
|
|
|
// show it
|
|
logDebug(g_conf.m_logDebugSummary, "sum: score=%08" PRId32" prescore=%08" PRId32" a=%05" PRId32" b=%05" PRId32" %s",
|
|
(int32_t)score,oldScore,(int32_t)a,(int32_t)b, xp.getBufStart());
|
|
|
|
// set lasta, besta, bestb
|
|
*lasta = a;
|
|
*besta = a;
|
|
*bestb = b;
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning %ld", score);
|
|
return score;
|
|
}
|
|
|
|
// get summary when no search terms could be found
|
|
bool Summary::getDefaultSummary(const Xml *xml, const Words *words, const Sections *sections, Pos *pos, int32_t maxSummaryLen) {
|
|
logTrace(g_conf.m_logTraceSummary, "BEGIN");
|
|
|
|
char *p = m_summary;
|
|
|
|
if (MAX_SUMMARY_LEN < maxSummaryLen) {
|
|
maxSummaryLen = MAX_SUMMARY_LEN;
|
|
}
|
|
|
|
// null it out
|
|
m_summaryLen = 0;
|
|
|
|
bool inLink = false;
|
|
char *pend = m_summary + maxSummaryLen - 2;
|
|
int32_t start = -1, numConsecutive = 0;
|
|
int32_t bestStart = -1;
|
|
int32_t bestEnd = -1;
|
|
int32_t longestConsecutive = 0;
|
|
int32_t lastAlnum = -1;
|
|
int32_t badFlags = NOINDEXFLAGS|SEC_IN_TITLE|SEC_IN_HEAD;
|
|
// shortcut
|
|
const nodeid_t *tids = words->getTagIds();
|
|
const int64_t *wids = words->getWordIds();
|
|
|
|
// get the section ptr array 1-1 with the words, "sp"
|
|
Section **sp = NULL;
|
|
if ( sections ) {
|
|
sp = sections->m_sectionPtrs;
|
|
}
|
|
|
|
for (int32_t i = 0;i < words->getNumWords(); i++){
|
|
// skip if in bad section
|
|
if ( sp && (sp[i]->m_flags & badFlags) ) {
|
|
continue;
|
|
}
|
|
|
|
if (start > 0 && bestStart == start &&
|
|
( words->getWord(i) - words->getWord(start) ) >=
|
|
( maxSummaryLen - 8 )){
|
|
longestConsecutive = numConsecutive;
|
|
bestStart = start;
|
|
bestEnd = lastAlnum;//i-1;
|
|
break;
|
|
}
|
|
if (words->isAlnum(i) ) {
|
|
if (!inLink) {
|
|
numConsecutive++;
|
|
}
|
|
lastAlnum = i;
|
|
if (start < 0) start = i;
|
|
continue;
|
|
}
|
|
nodeid_t tid = tids[i] & BACKBITCOMP;
|
|
// we gotta tag?
|
|
if ( tid ) {
|
|
// ignore <p> tags
|
|
if ( tid == TAG_P ) {
|
|
continue;
|
|
}
|
|
|
|
// is it a front tag?
|
|
if ( tid && ! (tids[i] & BACKBIT) ) {
|
|
if ( tid == TAG_A ) {
|
|
inLink = true;
|
|
}
|
|
}
|
|
else if ( tid ) {
|
|
if ( tid == TAG_A ) {
|
|
inLink = false;
|
|
}
|
|
}
|
|
|
|
if ( ! isBreakingTagId(tid) )
|
|
continue;
|
|
} else if ( ! wids[i] ) {
|
|
continue;
|
|
}
|
|
|
|
// end of consecutive words
|
|
if ( numConsecutive > longestConsecutive ) {
|
|
longestConsecutive = numConsecutive;
|
|
bestStart = start;
|
|
bestEnd = i-1;
|
|
}
|
|
start = -1;
|
|
numConsecutive = 0;
|
|
}
|
|
|
|
if (bestStart >= 0 && bestEnd > bestStart){
|
|
p += pos->filter( words, bestStart, bestEnd, true, p, pend - 10, xml->getVersion() );
|
|
|
|
// NULL terminate
|
|
*p++ = '\0';
|
|
|
|
// set length
|
|
m_summaryLen = p - m_summary;
|
|
|
|
if ( m_numDisplayLines > 0 ) {
|
|
m_displayLen = m_summaryLen;
|
|
}
|
|
|
|
if ( m_summaryLen > 50000 ) { g_process.shutdownAbort(true); }
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning true");
|
|
return true;
|
|
}
|
|
|
|
logTrace(g_conf.m_logTraceSummary, "END. Returning true");
|
|
return true;
|
|
}
|