본문 바로가기
🌔Developers

[Dev] 트위터 추천 알고리즘 핵심 소스코드 분석 + 일론 머스크의 소스 공개

by 키훈마스 2023. 4. 7.
반응형

일론 머스크의 트윗

 

일론 머스크가 깃허브에 트위터의 소스코드를 공개한 것으로 알려졌다. 

게다가, 일론 머스크는 이번 축하 행사에서 트위터 로고를 도지로 바꿨다.

 

머스크는 "우리의 초기 출시 알고리즘은 매우 당혹스러울 것이며 사람들은 많은 오류나 실수를 발견할 것이지만 우리(트위터 측)는 그것들을 매우 빨리 고칠 것이다. " 고 말했다.

 

오픈 소스 릴리스에는 아쉽게도 트위터의 광고 추천을 촉진하는 코드나 트위터의 추천 알고리즘을 훈련하기 위해 사용되는 데이터는 포함되어 있지 않다. 게다가 코드의 검사 방법이나 실제의 사용 방법에 관한 설명도 거의 포함되어 있지 않기 때문에, 릴리스는 개발자에 초점을 맞추고 있는 것을 강하게 볼 수 있다. 또한, 트위터의 클론을 만들어 보았더라도, 개인적으로 나는 위의 소스 코드를 직접 만들어본 트위터 클론에 적용하기는 어려워 보였다. 

 

트위터의 소스 코드 공개와 더불어, 당연하게도 트위터 사용자들의 사생활 및 개인 정보 침해에 대한 우려의 목소리가 곳곳에서 터져나왔다. 그러나 트위터는 "우리는 아동의 성적 착취와 조작에 대한 우리의 노력을 저해하는 것을 포함하여 사용자의 안전과 사생활, 또는 우리의 플랫폼을 나쁜 행위자들로부터 보호하는 능력을 침해하는 어떤 코드도 제외했다 "고 썼다. 이는 트윗이 윤리적인 AI와 신뢰 및 안전 스탭을 해고시킨지 몇 주 만에 나온 다소 엇갈린 메시지다. 하지만 그럼에도 불구하고 트위터 측은 코드 공개와 함께 "사용자 안전과 프라이버시가 보호되도록 하기 위한" 조치를 취했다고 주장하고 있다. 그러나 트위터의 오픈 소스가 공개된 이상, 크고 작은 문제는 분명히 발생할 것이라 생각된다.


개인적으로 나는 이번 트위터의 소스 코드 공개는 개발자들에게 큰 기회라고 생각된다. 필자 본인은 자바를 비롯한 소수의 언어들만의 작동하는 방식을 알아볼 수 있었지만, 그 외의 언어들은 나에게 굉장히 생소한 언어들로 가득했다. 아마 이번에 공개된 트위터의 소스 코드를 기반으로 하는 트위터의 '복제품'이 나오려면, 개인은 사실상 힘들거나 불가능할 것이며, 어느 정도 규모 있는 개발자 팀이 필요할 것이라 생각된다. 트위터 메인 페이지에는 추천 트윗을 선택하는 방법에 대한 정보가 담겨 있으며, 깃허브에서 볼 수 있다는 것은 SNS 서비스에서 초대형 추천 알고리즘을 볼 수 있는 기회다. 트위터는 전 세계에 10억명이 넘는 사용자들이 있으며, 트위터 추천 알고리즘의 소스코드 레파지토리에 들어가면 30만줄이 넘는 굉장히 많은 양의 코드로 작성되어 있는 것을 볼 수 있다.

소스 코드는 대부분 스칼라 언어로 작성되어 있다. 스칼라는 자바와 호환되는 언어이며 자바 대체어로 제공되었으며, JVM에서 구동된다.

JVM(자바-버츄얼-머신) 언어란?

JVM(Java Virtual Machine) 언어는 자바 가상 머신에서 실행되도록 설계된 프로그래밍 언어의 일종이다. JVM은 다양한 하드웨어와 운영 체제에서 자바 프로그램을 실행할 수 있는 런타임 환경을 제공하기 때문에 자바 플랫폼의 중요한 구성 요소이다.

자바는 JVM에서 실행되는 가장 잘 알려진 언어이지만, JVM과 함께 사용할 수 있는 유일한 언어는 아니다. 사실 스칼라, 코틀린, 그루비, 클로저를 포함하여 JVM에서 실행되도록 특별히 개발된 많은 다른 프로그래밍 언어들이 있다.

JVM에서 실행되는 언어를 사용하는 것의 주요 이점 중 하나는 다른 유형의 언어보다 많은 성능 이점을 제공할 수 있다는 것이다. JVM은 자바 코드의 실행을 최적화하도록 특별히 설계되었기 때문에 다른 언어보다 훨씬 빠른 수준의 성능을 제공할 수 있다.

JVM 언어의 또 다른 이점은 다른 자바 기반 응용 프로그램 및 프레임워크와의 높은 수준의 상호 운용성을 제공할 수 있다는 것이다. JVM은 자바 코드를 실행하기 위한 표준 플랫폼을 제공하기 때문에 JVM 기반 애플리케이션을 스프링이나 하이버네이트와 같은 다른 자바 기반 기술과 비교적 쉽게 통합할 수 있다.

JVM의 주요 기능 중 하나는 JVM에서 실행되는 애플리케이션에 높은 수준의 보안과 안정성을 제공한다는 것이다. JVM은 샌드박스 환경에서 실행되도록 설계되었기 때문에 버퍼 오버플로나 메모리 누수와 같은 광범위한 잠재적 보안 위협으로부터 애플리케이션을 보호할 수 있다.

또한 JVM은 강력하고 안정적인 애플리케이션을 더 쉽게 작성할 수 있는 여러 기능을 제공한다. 예를 들어 JVM에는 메모리 할당 및 할당 해제를 자동으로 관리하는 가비지 수집기가 포함되어 있어 다른 유형의 언어에서 흔히 발생할 수 있는 메모리 관련 오류를 방지하는 데 도움이 될 수 있다.

JVM 언어를 사용하여 애플리케이션을 개발하는 경우, 개발 프로세스를 간소화하는 데 도움이 될 수 있는 다양한 도구와 프레임워크가 있다. 예를 들어, 많은 JVM 언어들은 IntelliJ for Kotlin, Eclipse for Scala와 같이 그들만의 전용 IDE(통합 개발 환경)를 가지고 있다.

또한 스프링 부트 및 플레이 프레임워크를 포함하여 JVM 언어를 사용하여 웹 애플리케이션을 개발하는 데 사용할 수 있는 많은 인기 있는 프레임워크가 있다. 이러한 프레임워크는 개발 프로세스를 단순화하고 응용프로그램의 전반적인 품질을 향상시키는 데 도움이 될 수 있는 종속성 주입에 대한 기본 제공과 같은 다양한 기능을 제공한다.

 



가장 인기 있는 JVM 언어 중 하나는 IntelliJ IDE를 지원하는 JetBrains가 개발한 Kotlin이다. Kotlin은 Java에 대한 보다 간결하고 표현적인 대안이 되도록 설계되었으며 null safety 및 유형 추론과 같은 Java에서 사용할 수 없는 많은 기능을 제공한다.

또 다른 인기 있는 JVM 언어는 자바에 대한 더 강력하고 유연한 대안으로 설계된 스칼라이다. 스칼라는 함수형 프로그래밍 구조, 패턴 매칭 지원 등 자바에서 사용할 수 없는 수많은 기능을 제공한다.

그루비는 자바의 보다 유연하고 역동적인 대안이 되도록 설계된 또 다른 JVM 언어이다. 그루비는 개발 프로세스를 단순화하고 코드의 전반적인 품질을 향상시키는 데 도움이 되는 동적 타이핑 지원과 새로운 메소드로 자바 클래스를 확장하는 기능과 같은 다양한 기능을 제공한다.

마지막으로 Clojure는 JVM에서 실행되도록 설계된 기능적 프로그래밍 언어이다. Clojure는 불변의 데이터 구조에 대한 지원과 코드 동시 실행 기능과 같은 다른 JVM 언어에서 사용할 수 없는 다양한 기능을 제공한다.

 

이 언어는 자바스크립트와 비슷하며 역시 아직은 코틀린보다 자바가 옳을 것이다.

흥미로워 보이는 것들을 소개하자면, 파일을 보면 DB에서 트윗을 찍어 내 홈 화면에 올리면 주제에 따라 감점을 받고, 트렌드와 맞지 않거나 이미지가 포함된 트윗은 가점을 받는다. 또한 교호작용을 수행할 수 있으며, 각 교호작용에 대해 몇 점이 주어지는지 기록된다. 

아마도 그러한 기록들을 기반으로 트위터는 스팸을 차단하고, 여러 트렌드를 골구로 섞어 제공하는 데 큰 영향을 미쳤을 것이다.

리듬 파일에는 권장 알고리즘이 어떻게 진행되는지에 대한 정보가 포함되어 있다. 트위터는 보통 하루에 5~6억 개의 트윗을 생산하는데, 트위터의 사용자들은 메인 피드에 표시될 트윗을 선택하는 과정을 통해 모든 트윗을 얻습니다. 트윗의 소스 코드는 딥러닝 모델이고, 이는 각각의 트윗에 부합한 점수를 매긴 뒤 다른 사용자의 화면에 얼마나 표시될지를 결정한다. 트위터의 이 모델은 많은 좋아요와 리트윗 응답을 얻을 수 있는 트윗에 높은 점수를 주도록 훈련되었다. 그래서, 1,500개의 트윗 중에서 가장 높은 점수를 받은 트윗이 사용자에게 먼저 보여집니다. 이전에 필터링 과정을 거치면 사용자가 개인 블록을 꺼내 과도하게 중복된 콘텐츠를 제거하고 이렇게 한 다음 트윗을 사용자에게 보여준다.

트위터는 콘텐츠를 다루는 회사인데다, 거의 동일한 권장 알고리즘을 사용한다.

따라서 이번에 깃허브에 공개된 소스코드는 이러한 추천 알고리즘을 구현한 것으로,

개발자들에게 있어 트위터 추천 알고리즘에 대한 이해도가 높아질 것으로 보인다.

 

트위터의 추천 알고리즘 깃허브 주소 https://github.com/twitter/the-algorithm

 

GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

Source code for Twitter's Recommendation Algorithm - GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

github.com

해당 알고리즘 소스 코드는 위의 깃허브에서 받아볼 수 있다.

그리고 이 포스팅에서는 트위터에 작성된 대표적인 코드 몇 가지를 분석해볼 것이다.

단, 필자는 이 언어들 중 자바를 제외한 언어들에 자신이 없으므로 주로 자바 쪽을 소개하겠다.

트위터 추천 알고리즘AI의 자바 소스코드는 대부분 검색 결과 판정, 콘텐츠 뱌치 여부에 집중되어 있으며 자바답게 많은 양의 코드들이 안드로이드 기기에서만 동작하도록 설계되어 있다. 아마도 OS별 적합한 코드를 불러와 사용자에게 적용하는 방식일 것이며, 아이폰에는 Obj-C언어를 적용하는 등 거대한 빅테크인 트위터답게 많은 기기들을 위해 최적화가 잘 되어 있는 느낌을 받았다.

 

대다수의 한국 기업들과 스타트업들은 개발자 인력 수와 익숙한 언어를 고집하며 적은 양의 언어를 사용한다.

예를 들면, 서울특별시의 따릉이 어플리케이션은 사용자가 컴퓨터의 웹 브라우저 기반이든, 안드로이드이건 iOS이건 사용자에게 알맞는 기기 최적화 없이 대부분이 브라우저 기반인 자바스크립트로 동작하며, 배달의민족을 운영중인 우아한형제들, 비바리퍼블리카의 토스 같은 중견기업들 역시 사용자의 기기에 보여지는 것에 대하여 기기별로 작동하는 언어를 설정해놓는 최적화를 진행하였을 수 있지만, 중심부의 역할을 하는 핵심 모델 만큼은 데이터베이스를 위한 다양하지 않은 언어로 작성되어 있을 것이다.

 

그러나 아마 트위터를 비롯한 구글, 메다, 아마존, 바이두와 같은 세계의 빅 테크 기업들은 사용자의 기기와 OS별로 최적화된 코드를 불러와 사용하는 것을 선호하는 듯 하다. 무엇이든, 세세한 부분까지도 말이다.

 

이를 초보 개발자들이 알아들을 수 있게 설명하라면,

하나의 서비스 앱(SaaS)을 만들 때 자바 또는 코틀린, 자바스크립트와 CSS, 스위프트와 Obj-C를 사용해 만들 것이냐, 아니면 그냥 간단하게 크로스 플랫폼 언어인 자마린(Xamarin C#)으로 만들 것이냐의 차이가 될 수 있을 것이다.

 

후자의 경우 속도는 현저히 느려지겠지만, 대신 개발 시간을 단축하고 한 번의 패치로 거의 모든 기기에 적용시킬 수 있기 때문에 개발자의 규모가 적은 스타트업이나 개인이 서비스를 만들 때 굉장히 많이 선호되는 방식이다. 

그러나 전자의 방식을 채택하면, 기기별로 속도도 빨라질 것이며 보안이 한 층 더 업그레이드 될 수 있을 것이다.

이 방식을 채택하기 위해서는 다양한 언어의 개발자들이 필요하거나, 다양한 언어를 알고 있는 편이 좋을 듯.

개인적으로 나 역시 후자의 방식을 선호한다.

 

이제 트위터 오픈 소스의 코드 분석으로 들어가보자.

먼저 첫 번째로, SpamVectorScoringFunction.java 파일을 예시로 들어보자.

 

GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

Source code for Twitter's Recommendation Algorithm - GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

github.com

 

//트위터  스팸감지

package com.twitter.search.earlybird.search.relevance.scoring;

import java.io.IOException;

import com.google.common.annotations.VisibleForTesting;

import org.apache.lucene.search.Explanation;

import com.twitter.search.common.relevance.features.RelevanceSignalConstants;
import com.twitter.search.common.schema.base.ImmutableSchemaInterface;
import com.twitter.search.common.schema.earlybird.EarlybirdFieldConstants.EarlybirdFieldConstant;
import com.twitter.search.earlybird.common.config.EarlybirdConfig;
import com.twitter.search.earlybird.thrift.ThriftSearchResultMetadata;
import com.twitter.search.earlybird.thrift.ThriftSearchResultMetadataOptions;
import com.twitter.search.earlybird.thrift.ThriftSearchResultsRelevanceStats;

public class SpamVectorScoringFunction extends ScoringFunction {
  private static final int MIN_TWEEPCRED_WITH_LINK =
      EarlybirdConfig.getInt("min_tweepcred_with_non_whitelisted_link", 25);

  // The engagement threshold that prevents us from filtering users with low tweepcred.
  private static final int ENGAGEMENTS_NO_FILTER = 1;

  @VisibleForTesting
  static final float NOT_SPAM_SCORE = 0.5f;
  @VisibleForTesting
  static final float SPAM_SCORE = -0.5f;

  public SpamVectorScoringFunction(ImmutableSchemaInterface schema) {
    super(schema);
  }

  @Override
  protected float score(float luceneQueryScore) throws IOException {
    if (documentFeatures.isFlagSet(EarlybirdFieldConstant.FROM_VERIFIED_ACCOUNT_FLAG)) {
      return NOT_SPAM_SCORE;
    }

    int tweepCredThreshold = 0;
    if (documentFeatures.isFlagSet(EarlybirdFieldConstant.HAS_LINK_FLAG)
        && !documentFeatures.isFlagSet(EarlybirdFieldConstant.HAS_IMAGE_URL_FLAG)
        && !documentFeatures.isFlagSet(EarlybirdFieldConstant.HAS_VIDEO_URL_FLAG)
        && !documentFeatures.isFlagSet(EarlybirdFieldConstant.HAS_NEWS_URL_FLAG)) {
      // Contains a non-media non-news link, definite spam vector.
      tweepCredThreshold = MIN_TWEEPCRED_WITH_LINK;
    }

    int tweepcred = (int) documentFeatures.getFeatureValue(EarlybirdFieldConstant.USER_REPUTATION);

    // For new user, tweepcred is set to a sentinel value of -128, specified at
    // src/thrift/com/twitter/search/common/indexing/status.thrift
    if (tweepcred >= tweepCredThreshold
        || tweepcred == (int) RelevanceSignalConstants.UNSET_REPUTATION_SENTINEL) {
      return NOT_SPAM_SCORE;
    }

    double retweetCount =
        documentFeatures.getUnnormalizedFeatureValue(EarlybirdFieldConstant.RETWEET_COUNT);
    double replyCount =
        documentFeatures.getUnnormalizedFeatureValue(EarlybirdFieldConstant.REPLY_COUNT);
    double favoriteCount =
        documentFeatures.getUnnormalizedFeatureValue(EarlybirdFieldConstant.FAVORITE_COUNT);

    // If the tweet has enough engagements, do not mark it as spam.
    if (retweetCount + replyCount + favoriteCount >= ENGAGEMENTS_NO_FILTER) {
      return NOT_SPAM_SCORE;
    }

    return SPAM_SCORE;
  }

  @Override
  protected Explanation doExplain(float luceneScore) {
    return null;
  }

  @Override
  public ThriftSearchResultMetadata getResultMetadata(ThriftSearchResultMetadataOptions options) {
    return null;
  }

  @Override
  public void updateRelevanceStats(ThriftSearchResultsRelevanceStats relevanceStats) {
  }
}

위의 코드를 살펴보자면, 위의 코드는 주로 안드로이드인 모바일용으로 설계된 언어인 자바로 작성되었으며, 이는 자바 가상 머신(JVM)이 설치된 모든 기계에서 실행될 수 있음을 의미한다. 코드는 "com.twitter"라는 이름의 패키지의 일부입니다.일찍 일어나는 새를 찾다.search.dll.dll"을 사용하며, 다양한 요인을 기반으로 검색 결과의 관련성을 결정하는 검색 엔진에 대한 스코어링 기능을 구현하는 역할을 담당한다.

이 코드에 정의된 클래스는 "SpamVectorScoringFunction"이고 "ScoringFunction"이라는 다른 클래스를 확장합니다. "score", "doExplain" 및 "getResultMetadata"의 세 가지 메서드를 재정의한다. "점수" 메서드는 float 인수를 사용하고 float 값을 반환하며. 인수는 검색 엔진에 의해 결정된 쿼리 결과의 점수를 나타내는 것 같다.

 

아마도 이 코드는 뉴스 링크가 아닌(일명 가짜뉴스 등) 뉴스 링크를 포함하는지 여부, 작성자의 사용자 평판, 트윗이 받은 리트윗, 응답 및 즐겨찾기 수 등 결과와 관련된 문서의 다양한 기능을 사용합니다. 이러한 기능에 따라 메소드는 문서가 스팸인지 여부를 나타내는 점수를 돌려주는 식으로 작동한다. 문서가 스팸인 것으로 확인되면, 문서는 마이너스, 음의 점수를 반환하고, 그렇지 않으면 플러스, 좋은 점수인 양의 점수를 반환하는 것으로 보인다.

아마도 그동안 트위터는 위의 방법을 통해 수많은 가짜 뉴스들을 걸러왔을 것이다.

 

이제 AntiGamingFilter.java 를 살펴보자.

 

GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

Source code for Twitter's Recommendation Algorithm - GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

github.com

 

package com.twitter.search.earlybird.search;

import java.io.IOException;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import com.google.common.annotations.VisibleForTesting;

import org.apache.commons.lang.mutable.MutableInt;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreMode;

import com.twitter.common_internal.collections.RandomAccessPriorityQueue;
import com.twitter.search.common.schema.earlybird.EarlybirdFieldConstants.EarlybirdFieldConstant;
import com.twitter.search.common.search.TwitterIndexSearcher;
import com.twitter.search.common.util.analysis.LongTermAttributeImpl;
import com.twitter.search.core.earlybird.index.EarlybirdIndexSegmentAtomicReader;

public class AntiGamingFilter {
  private interface Acceptor {
    boolean accept(int internalDocID) throws IOException;
  }

  private NumericDocValues userReputation;
  private NumericDocValues fromUserIDs;

  private final Query luceneQuery;

  private boolean termsExtracted = false;
  private final Set<Term> queryTerms;

  // we ignore these user ids for anti-gaming filtering, because they were explicitly queried for
  private Set<Long> segmentUserIDWhitelist = null;
  // we gather the whitelisted userIDs from all segments here
  private Set<Long> globalUserIDWhitelist = null;

  /**
   * Used to track the number of occurrences of a particular user.
   */
  private static final class UserCount
      implements RandomAccessPriorityQueue.SignatureProvider<Long> {
    private long userID;
    private int count;

    @Override
    public Long getSignature() {
      return userID;
    }

    @Override
    public void clear() {
      userID = 0;
      count = 0;
    }
  }

  private static final Comparator<UserCount> USER_COUNT_COMPARATOR =
      (d1, d2) -> d1.count == d2.count ? Long.compare(d1.userID, d2.userID) : d1.count - d2.count;

  private final RandomAccessPriorityQueue<UserCount, Long> priorityQueue =
      new RandomAccessPriorityQueue<UserCount, Long>(1024, USER_COUNT_COMPARATOR) {
    @Override
    protected UserCount getSentinelObject() {
      return new UserCount();
    }
  };

  private final Acceptor acceptor;
  private final int maxHitsPerUser;

  /**
   * Creates an AntiGamingFilter that either accepts or rejects tweets from all users.
   * This method should only be called in tests.
   *
   * @param alwaysValue Determines if tweets should always be accepted or rejected.
   * @return An AntiGamingFilter that either accepts or rejects tweets from all users.
   */
  @VisibleForTesting
  public static AntiGamingFilter newMock(boolean alwaysValue) {
    return new AntiGamingFilter(alwaysValue) {
      @Override
      public void startSegment(EarlybirdIndexSegmentAtomicReader reader) {
      }
    };
  }

  private AntiGamingFilter(boolean alwaysValue) {
    acceptor = internalDocID -> alwaysValue;
    maxHitsPerUser = Integer.MAX_VALUE;
    termsExtracted = true;
    luceneQuery = null;
    queryTerms = null;
  }

  public AntiGamingFilter(int maxHitsPerUser, int maxTweepCred, Query luceneQuery) {
    this.maxHitsPerUser = maxHitsPerUser;
    this.luceneQuery = luceneQuery;

    if (maxTweepCred != -1) {
      this.acceptor = internalDocID -> {
        long userReputationVal =
            userReputation.advanceExact(internalDocID) ? userReputation.longValue() : 0L;
        return ((byte) userReputationVal > maxTweepCred) || acceptUser(internalDocID);
      };
    } else {
      this.acceptor = this::acceptUser;
    }

    this.queryTerms = new HashSet<>();
  }

  public Set<Long> getUserIDWhitelist() {
    return globalUserIDWhitelist;
  }

  private boolean acceptUser(int internalDocID) throws IOException {
    final long fromUserID = getUserId(internalDocID);
    final MutableInt freq = new MutableInt();
    // try to increment UserCount for an user already exist in the priority queue.
    boolean incremented = priorityQueue.incrementElement(
        fromUserID, element -> freq.setValue(++element.count));

    // If not incremented, it means the user node does not exist in the priority queue yet.
    if (!incremented) {
      priorityQueue.updateTop(element -> {
        element.userID = fromUserID;
        element.count = 1;
        freq.setValue(element.count);
      });
    }

    if (freq.intValue() <= maxHitsPerUser) {
      return true;
    } else if (segmentUserIDWhitelist == null) {
      return false;
    }
    return segmentUserIDWhitelist.contains(fromUserID);
  }

  /**
   * Initializes this filter with the new feature source. This method should be called every time an
   * earlybird searcher starts searching in a new segment.
   *
   * @param reader The reader for the new segment.
   */
  public void startSegment(EarlybirdIndexSegmentAtomicReader reader) throws IOException {
    if (!termsExtracted) {
      extractTerms(reader);
    }

    fromUserIDs =
        reader.getNumericDocValues(EarlybirdFieldConstant.FROM_USER_ID_CSF.getFieldName());

    // fill the id whitelist for the current segment.  initialize lazily.
    segmentUserIDWhitelist = null;

    SortedSet<Integer> sortedFromUserDocIds = new TreeSet<>();
    for (Term t : queryTerms) {
      if (t.field().equals(EarlybirdFieldConstant.FROM_USER_ID_FIELD.getFieldName())) {
        // Add the operand of the from_user_id operator to the whitelist
        long fromUserID = LongTermAttributeImpl.copyBytesRefToLong(t.bytes());
        addUserToWhitelists(fromUserID);
      } else if (t.field().equals(EarlybirdFieldConstant.FROM_USER_FIELD.getFieldName())) {
        // For a [from X] filter, we need to find a document that has the from_user field set to X,
        // and then we need to get the value of the from_user_id field for that document and add it
        // to the whitelist. We can get the from_user_id value from the fromUserIDs NumericDocValues
        // instance, but we need to traverse it in increasing order of doc IDs. So we add a doc ID
        // for each term to a sorted set for now, and then we traverse it in increasing doc ID order
        // and add the from_user_id values for those docs to the whitelist.
        int firstInternalDocID = reader.getNewestDocID(t);
        if (firstInternalDocID != EarlybirdIndexSegmentAtomicReader.TERM_NOT_FOUND) {
          sortedFromUserDocIds.add(firstInternalDocID);
        }
      }
    }

    for (int fromUserDocId : sortedFromUserDocIds) {
      addUserToWhitelists(getUserId(fromUserDocId));
    }

    userReputation =
        reader.getNumericDocValues(EarlybirdFieldConstant.USER_REPUTATION.getFieldName());

    // Reset the fromUserIDs NumericDocValues so that the acceptor can use it to iterate over docs.
    fromUserIDs =
        reader.getNumericDocValues(EarlybirdFieldConstant.FROM_USER_ID_CSF.getFieldName());
  }

  private void extractTerms(IndexReader reader) throws IOException {
    Query query = luceneQuery;
    for (Query rewrittenQuery = query.rewrite(reader); rewrittenQuery != query;
         rewrittenQuery = query.rewrite(reader)) {
      query = rewrittenQuery;
    }

    // Create a new TwitterIndexSearcher instance here instead of an IndexSearcher instance, to use
    // the TwitterIndexSearcher.collectionStatistics() implementation.
    query.createWeight(new TwitterIndexSearcher(reader), ScoreMode.COMPLETE, 1.0f)
        .extractTerms(queryTerms);
    termsExtracted = true;
  }

  public boolean accept(int internalDocID) throws IOException {
    return acceptor.accept(internalDocID);
  }

  private void addUserToWhitelists(long userID) {
    if (this.segmentUserIDWhitelist == null) {
      this.segmentUserIDWhitelist = new HashSet<>();
    }
    if (this.globalUserIDWhitelist == null) {
      this.globalUserIDWhitelist = new HashSet<>();
    }
    this.segmentUserIDWhitelist.add(userID);
    this.globalUserIDWhitelist.add(userID);
  }

  @VisibleForTesting
  protected long getUserId(int internalDocId) throws IOException {
    return fromUserIDs.advanceExact(internalDocId) ? fromUserIDs.longValue() : 0L;
  }
}

이 코드는 "AntiGamingFilter"라고 불리는 필터를 자바로 구현한 것이다. 

비슷한 내용의 트윗을 다수 올리는 사용자를 적발해 스팸이나 저질 트윗을 식별해 제거하는 데 사용되는 것으로 보이며,

큰 트윗 인덱스를 검색하고 어떤 트윗을 제외해야 하는지 결정하는 듯.

필터는 사용자가 자신이 올린 다른 트윗과 동일한 키워드를 포함하는 트윗을 올린 횟수를 세는 방식으로 작동하는 것 처럼 보인다. 

사용자가 유사한 트윗을 너무 많이 게시한 경우 해당 트윗은 잠재적으로 스팸으로 낙인이 찍히고 (찍히면 잣 되는 겁니다.) 해당 트윗은 검색 결과에서 제거된다. 필터는 사용자의 유명도, 명성(?)과 관련해서 사용자당 최대 적중 횟수를 고려하는 것으로 보인다.

쉽게 말해, 수 많은 팔로워를 가진 정치인들이나 기업가, 인플루언서들은 트윗을 많이 날려대도 큰 제한을 받지 않지만 그렇지 않은 우리같은 일반인들은 트윗을 많이 날려대면 제한을 먹을 것이라는 소리다.

마지막으로 알아볼 예시는 ClientIdWhitelistFilter.java 이다.

 

GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

Source code for Twitter's Recommendation Algorithm - GitHub - twitter/the-algorithm: Source code for Twitter's Recommendation Algorithm

github.com

package com.twitter.search.feature_update_service.filters;

import com.google.inject.Inject;
import com.google.inject.Singleton;

import com.twitter.finagle.Service;
import com.twitter.finatra.thrift.AbstractThriftFilter;
import com.twitter.finatra.thrift.ThriftRequest;
import com.twitter.inject.annotations.Flag;
import com.twitter.search.common.metrics.SearchRateCounter;
import com.twitter.search.feature_update_service.thriftjava.FeatureUpdateResponse;
import com.twitter.search.feature_update_service.thriftjava.FeatureUpdateResponseCode;
import com.twitter.search.feature_update_service.whitelist.ClientIdWhitelist;
import com.twitter.util.Future;

@Singleton
public class ClientIdWhitelistFilter extends AbstractThriftFilter {
  private final boolean enabled;
  private final ClientIdWhitelist whitelist;

  private final SearchRateCounter unknownClientIdStat =
      SearchRateCounter.export("unknown_client_id");
  private final SearchRateCounter noClientIdStat =
      SearchRateCounter.export("no_client_id");

  @Inject
  public ClientIdWhitelistFilter(
      ClientIdWhitelist whitelist,
      @Flag("client.whitelist.enable") Boolean enabled
  ) {
    this.whitelist = whitelist;
    this.enabled = enabled;
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T, R> Future<R> apply(ThriftRequest<T> request, Service<ThriftRequest<T>, R> svc) {
    if (!enabled) {
      return svc.apply(request);
    }
    if (request.clientId().isEmpty()) {
      noClientIdStat.increment();
      return (Future<R>) Future.value(
          new FeatureUpdateResponse(FeatureUpdateResponseCode.MISSING_CLIENT_ERROR)
              .setDetailMessage("finagle clientId is required in request"));

    } else if (!whitelist.isClientAllowed(request.clientId().get())) {
      // It's safe to use get() in the above condition because
      // clientId was already checked for emptiness
      unknownClientIdStat.increment();
      return (Future<R>) Future.value(
          new FeatureUpdateResponse(FeatureUpdateResponseCode.UNKNOWN_CLIENT_ERROR)
              .setDetailMessage(String.format(
                  "request contains unknown finagle clientId: %s", request.clientId().toString())));
    } else {
      return svc.apply(request);
    }
  }
}

이 코드는 화이트리스트를 기준으로 절약 요청에 포함된 클라이언트 ID가 허용되는지 여부를 확인하고, 클라이언트 ID가 허용되지 않으면 요청을 거부하는 소스코드다.

이 클래스의 적용 방법은 먼저 클라이언트에서 지정한 대로 필터가 활성화되었는지 확인한다.화이트리스트.enable 플래그입니다. 필터가 활성화되지 않은 경우 요청은 변경되지 않고 서비스로 전달됩니다. 그렇지 않으면 메소드는 요청에 클라이언트 ID가 포함되어 있는지 확인합니다. 클라이언트 ID가 누락된 경우 Method는 MISSING_CLIENT_ERROR라는 오류 코드를 가진 응답을 가져가며, 트위터 사용자 아이디를 포함한 클라이언트 ID가 있으면 메소드는 클라이언트 ID가 화이트리스트에 있는지 확인합니다. 클라이언트 ID가 화이트리스트에 없는 경우 메서드는 UNKNOWN_CLIENT_ERROR라는 오류 코드로 응답을 반환합니다. 그렇지 않으면 메소드는 요청을 서비스로 전달합니다. 한마디로 밴 여부를 결정짓는 소스코드다. 잘만 이용하면 트위터에서 밴 안먹고 생존할 수도 있다.

 

이상으로 트위터 일부 자바 소스 코드에 대한 분석과 나의 견해를 마친다.

트위터의 추천 인공지능 알고리즘은 자바를 기반으로 하는 다수의 언어와 트위터를 사용하게 될 거의 모든 사용자들의 기기에 최적화할수 있는 언어로 작성되어 있으며, 핵심 코드마저도 사용자들의 기기에서 최적을 기반으로 작동할 수 있는 언어로 작성되어 있다는 것을 확인할 수 있었다.

 

본 블로그 포스팅을 유튜브 등 어딘가에 활용하시고 싶다면, 출처는 꼭 남겨주세요오

만약 도움이 되었다면 하트를 눌러주세요-! 감사합니다-!!🙂🙂

 

 

 

반응형